Rename IdentityPrismProvider => PointerToPrismProvider

This commit is contained in:
Aria Minaei 2023-01-16 17:25:44 +01:00
parent 2b396a5295
commit 155c84523a
6 changed files with 362 additions and 32 deletions

View file

@ -22,19 +22,17 @@ enum ValueTypes {
/** /**
* Interface for objects that can provide a prism at a certain path. * Interface for objects that can provide a prism at a certain path.
*/ */
export interface IdentityPrismProvider { export interface PointerToPrismProvider {
/** /**
* @internal * @internal
* Future: We could consider using a `Symbol.for("dataverse/IdentityPrismProvider")` as a key here, similar to * Future: We could consider using a `Symbol.for("dataverse/PointerToPrismProvider")` as a key here, similar to
* how {@link Iterable} works for `of`. * how {@link Iterable} works for `of`.
*/ */
readonly $$isIdentityPrismProvider: true readonly $$isPointerToPrismProvider: true
/** /**
* Returns a prism of the value at the provided path. * Returns a prism of the value at the provided path.
*
* @param path - The path to create the prism at.
*/ */
getIdentityPrism(path: Array<string | number>): Prism<unknown> pointerToPrism<P>(pointer: Pointer<P>): Prism<P>
} }
const getTypeOfValue = (v: unknown): ValueTypes => { const getTypeOfValue = (v: unknown): ValueTypes => {
@ -115,12 +113,12 @@ class Scope {
/** /**
* Wraps an object whose (sub)properties can be individually tracked. * Wraps an object whose (sub)properties can be individually tracked.
*/ */
export default class Atom<State> implements IdentityPrismProvider { export default class Atom<State> implements PointerToPrismProvider {
private _currentState: State private _currentState: State
/** /**
* @internal * @internal
*/ */
readonly $$isIdentityPrismProvider = true readonly $$isPointerToPrismProvider = true
private readonly _rootScope: Scope private readonly _rootScope: Scope
/** /**
* Convenience property that gives you a pointer to the root of the atom. * Convenience property that gives you a pointer to the root of the atom.
@ -128,14 +126,15 @@ export default class Atom<State> implements IdentityPrismProvider {
* @remarks * @remarks
* Equivalent to `pointer({ root: thisAtom, path: [] })`. * Equivalent to `pointer({ root: thisAtom, path: [] })`.
*/ */
readonly pointer: Pointer<State> readonly pointer: Pointer<State> = pointer({root: this as $FixMe, path: []})
readonly prism: Prism<State> = this.getIdentityPrism([]) as $IntentionalAny readonly prism: Prism<State> = this.pointerToPrism(
this.pointer,
) as $IntentionalAny
constructor(initialState: State) { constructor(initialState: State) {
this._currentState = initialState this._currentState = initialState
this._rootScope = new Scope(undefined, []) this._rootScope = new Scope(undefined, [])
this.pointer = pointer({root: this as $FixMe, path: []})
} }
/** /**
@ -232,7 +231,8 @@ export default class Atom<State> implements IdentityPrismProvider {
* *
* @param path - The path to create the prism at. * @param path - The path to create the prism at.
*/ */
getIdentityPrism(path: Array<string | number>): Prism<unknown> { pointerToPrism<P>(pointer: Pointer<P>): Prism<P> {
const {path} = getPointerParts(pointer)
const subscribe = (listener: (val: unknown) => void) => const subscribe = (listener: (val: unknown) => void) =>
this._onPathValueChange(path, listener) this._onPathValueChange(path, listener)
@ -240,7 +240,7 @@ export default class Atom<State> implements IdentityPrismProvider {
return prism(() => { return prism(() => {
return prism.source(subscribe, getValue) return prism.source(subscribe, getValue)
}) }) as Prism<P>
} }
} }
@ -260,23 +260,22 @@ export const pointerToPrism = <P extends PointerType<$IntentionalAny>>(
let prismInstance = identifyPrismWeakMap.get(meta) let prismInstance = identifyPrismWeakMap.get(meta)
if (!prismInstance) { if (!prismInstance) {
const root = meta.root const root = meta.root
if (!isIdentityPrismProvider(root)) { if (!isPointerToPrismProvider(root)) {
throw new Error( throw new Error(
`Cannot run pointerToPrism() on a pointer whose root is not an IdentityPrismProvider`, `Cannot run pointerToPrism() on a pointer whose root is not an PointerToPrismProvider`,
) )
} }
const {path} = meta prismInstance = root.pointerToPrism(pointer as $IntentionalAny)
prismInstance = root.getIdentityPrism(path)
identifyPrismWeakMap.set(meta, prismInstance) identifyPrismWeakMap.set(meta, prismInstance)
} }
return prismInstance as $IntentionalAny return prismInstance as $IntentionalAny
} }
function isIdentityPrismProvider(val: unknown): val is IdentityPrismProvider { function isPointerToPrismProvider(val: unknown): val is PointerToPrismProvider {
return ( return (
typeof val === 'object' && typeof val === 'object' &&
val !== null && val !== null &&
(val as $IntentionalAny)['$$isIdentityPrismProvider'] === true (val as $IntentionalAny)['$$isPointerToPrismProvider'] === true
) )
} }

View file

@ -1,9 +1,11 @@
import type {IdentityPrismProvider} from './Atom' import type {PointerToPrismProvider} from './Atom'
import Atom, {val} from './Atom' import Atom, {val} from './Atom'
import type {Pointer} from './pointer' import type {Pointer} from './pointer'
import {getPointerMeta} from './pointer'
import pointer from './pointer' import pointer from './pointer'
import type {$FixMe, $IntentionalAny} from './types' import type {$FixMe, $IntentionalAny} from './types'
import prism from './prism/prism' import prism from './prism/prism'
import type {Prism} from './prism/Interface'
/** /**
* Allows creating pointer-prisms where the pointer can be switched out. * Allows creating pointer-prisms where the pointer can be switched out.
@ -13,12 +15,12 @@ import prism from './prism/prism'
* to the proxied pointer too. * to the proxied pointer too.
*/ */
export default class PointerProxy<O extends {}> export default class PointerProxy<O extends {}>
implements IdentityPrismProvider implements PointerToPrismProvider
{ {
/** /**
* @internal * @internal
*/ */
readonly $$isIdentityPrismProvider = true readonly $$isPointerToPrismProvider = true
private readonly _currentPointerBox: Atom<Pointer<O>> private readonly _currentPointerBox: Atom<Pointer<O>>
/** /**
* Convenience pointer pointing to the root of this PointerProxy. * Convenience pointer pointing to the root of this PointerProxy.
@ -46,14 +48,15 @@ export default class PointerProxy<O extends {}>
* *
* @param path - The path to create the prism at. * @param path - The path to create the prism at.
*/ */
getIdentityPrism(path: Array<string | number>) { pointerToPrism<P>(pointer: Pointer<P>): Prism<P> {
const {path} = getPointerMeta(pointer)
return prism(() => { return prism(() => {
const currentPointer = this._currentPointerBox.prism.getValue() const currentPointer = this._currentPointerBox.prism.getValue()
const subPointer = path.reduce( const subPointer = path.reduce(
(pointerSoFar, pathItem) => (pointerSoFar as $IntentionalAny)[pathItem], (pointerSoFar, pathItem) => (pointerSoFar as $IntentionalAny)[pathItem],
currentPointer, currentPointer,
) )
return val(subPointer) return val(subPointer) as P
}) })
} }
} }

View file

@ -0,0 +1,327 @@
import {prism} from '@theatre/dataverse'
describe(`prism()`, () => {
// these tests are going will act as a guide on how to use prisms.
// prisms are a way to create a value that depends on other values.
// prisms can be hot or cold, they have dependencies and dependents, and hot prisms can be stale or fresh.
// let's start with a simple example:
test(`using a pretty useless prism`, async () => {
// each prism has a calculate function that it runs to calculate its value. let's make a simple function that just returns 1
const calculate = jest.fn(() => 1)
// now we can make a prism out of it
const pr = prism(calculate)
// now, this prism is pretty useless. it doesn't depend on anything, and it doesn't have any dependents.
// but we can already illustrate some of the concepts that are important to understand prisms.
// `calculate` won't be called until it's needed
expect(calculate).not.toHaveBeenCalled()
// we can get the value of the prism, which will be what `calculate` returned
expect(pr.getValue()).toBe(1)
// now the calculate function will have be called
expect(calculate).toHaveBeenCalledTimes(1)
// now, you might expect that if we call `getValue()` again, the calculate function won't be called again.
// but that's not the case. the calculate function will be called again, because the prism is cold.
// we'll talk about cold/hot in a bit, but let's just say that cold prisms are calculated every time they're read.
pr.getValue()
expect(calculate).toHaveBeenCalledTimes(2)
// we can even check whether a prism is hot or cold. Ours is cold.
expect(pr.isHot).toBe(false)
// we'll get to hot prisms soon, but let's talk about dependencies and dependents first.
})
// prisms can depend on other prisms. let's make a prism that depends on another prism.
test(`prisms can depend on other prisms`, async () => {
const calculateA = jest.fn(() => 1)
const a = prism(calculateA)
const calculateATimesTwo = jest.fn(() => a.getValue() * 2)
const aTimesTwo = prism(calculateATimesTwo)
// clear the count of mocks
function clearMocks() {
calculateA.mockClear()
calculateATimesTwo.mockClear()
}
// now, `aTimesTwo` depends on `a`.
// In our parlance, `aTimesTwo` is a dependent of `a`, and `a` is a dependency of `aTimesTwo`.
// now if we read the value of `aTimesTwo`, it will call `calculateATimesTwo`, which will call `calculateA`:
expect(aTimesTwo.getValue()).toBe(2)
expect(calculateA).toHaveBeenCalledTimes(1)
expect(calculateATimesTwo).toHaveBeenCalledTimes(1)
clearMocks()
// and like we saw in the previous test, if we read the value of `aTimesTwo` again, it will call both of our calculate functions again:
aTimesTwo.getValue()
expect(calculateATimesTwo).toHaveBeenCalledTimes(1)
expect(calculateA).toHaveBeenCalledTimes(1)
clearMocks()
// but if we read the value of `a`, it won't call `calculateATimesTwo`:
a.getValue()
expect(calculateATimesTwo).toHaveBeenCalledTimes(0)
expect(calculateA).toHaveBeenCalledTimes(1)
clearMocks()
// now let's see what happens if we make our prism hot.
// one way to do this, is to add an `onStale` listener to our prism.
const onStale = jest.fn()
const unsubscribe = aTimesTwo.onStale(onStale)
// as soon as we do this, the prism will become hot.
expect(aTimesTwo.isHot).toBe(true)
// and so will its dependencies:
expect(a.isHot).toBe(true)
// so let's see what happens when we read the value of `aTimesTwo` again:
aTimesTwo.getValue()
// `calculateATimesTwo` will be called again,
expect(calculateATimesTwo).toHaveBeenCalledTimes(1)
// and so will `calculateA`,
expect(calculateA).toHaveBeenCalledTimes(1)
clearMocks()
// but if we read `aTimesTwo` again, none of the calculate functions will be called again.
aTimesTwo.getValue()
expect(calculateATimesTwo).toHaveBeenCalledTimes(0)
expect(calculateA).toHaveBeenCalledTimes(0)
clearMocks()
// this behavior propogates up the dependency chain, so if we read `a` again, `calculateA` won't be called again,
// because its value is already fresh.
a.getValue()
expect(calculateA).toHaveBeenCalledTimes(0)
clearMocks()
// at this point, since none of our prisms depend on a prism whose value will change, they'll remain
// fresh forever.
a.getValue()
aTimesTwo.getValue()
a.getValue()
aTimesTwo.getValue()
expect(calculateATimesTwo).toHaveBeenCalledTimes(0)
expect(calculateA).toHaveBeenCalledTimes(0)
clearMocks()
// but as soon as we unsubscribe from our `onStale()` listener, the prisms will become cold again.
unsubscribe()
expect(aTimesTwo.isHot).toBe(false)
expect(a.isHot).toBe(false)
// and they'll return back to their cold behavior.
aTimesTwo.getValue()
expect(calculateATimesTwo).toHaveBeenCalledTimes(1)
expect(calculateA).toHaveBeenCalledTimes(1)
clearMocks()
aTimesTwo.getValue()
expect(calculateATimesTwo).toHaveBeenCalledTimes(1)
expect(calculateA).toHaveBeenCalledTimes(1)
clearMocks()
// now, one more thing before we move on. What will if we make `a` hot, but not `aTimesTwo`?
// let's try it out.
const unsubcribeFromAOnStale = a.onStale(() => {})
// a will go hot
expect(a.isHot).toBe(true)
// but aTimesTwo will stay cold
expect(aTimesTwo.isHot).toBe(false)
// now let's read the value of `a`
a.getValue()
// `calculateA` will be called
expect(calculateA).toHaveBeenCalledTimes(1)
// and obviously `calculateATimesTwo` won't be called
expect(calculateATimesTwo).toHaveBeenCalledTimes(0)
clearMocks()
// and if we re-read the value of `a`, `calculateA` won't be called again, becuase `a` is hot and its value is fresh.
a.getValue()
expect(calculateA).toHaveBeenCalledTimes(0)
clearMocks()
// but if we read the value of `aTimesTwo`, `calculateATimesTwo` will be called, because `aTimesTwo` is cold.
aTimesTwo.getValue()
expect(calculateATimesTwo).toHaveBeenCalledTimes(1)
// yet `calculateA` won't be called, because `a` is hot and its value is fresh.
expect(calculateA).toHaveBeenCalledTimes(0)
clearMocks()
// in conclusion, if we make a prism hot, it'll make its dependencies hot too.
// if we read the value of a cold prism, it'll call its calculate function, which will
// call the calculate functions of its dependencies, and so on.
// but if we read the value of a hot prism, it'll only call its calculate function if its value is stale.
// le'ts wrap up this part by unsubscribing from `a`'s `onStale` listener to not have any lingering listeners.
unsubcribeFromAOnStale()
})
// so far, our prisms have not depended on any changing values, so in turn, _their_ values have never changed either.
// but what if we want to create a prism that depends on a changing value?
// we call those values "sources", and we can create them using the `prism.source()` hook:
test('prism.source()', () => {
// let's say we want to create a prism that depends on this value:
let value = 0
{
// the _wrong_ way to do this, is to create a prism that directly reads this value
const p = prism(() => value)
// this will actually work if the prism is cold:
expect(p.getValue()).toBe(0)
value = 1
expect(p.getValue()).toBe(1)
// but if we make the prism hot, it'll never update its value, because it's not subscribed to any sources.
const unsubscribe = p.onStale(() => {})
expect(p.isHot).toBe(true)
// on first read, it'll give us the current value of `value`, which is 1.
expect(p.getValue()).toBe(1)
// but if we change `value` again, the prism won't know
value = 2
expect(p.getValue()).toBe(1)
// and so it'll keep returning the old value.
expect(p.getValue()).toBe(1)
unsubscribe()
}
// so, the _right_ way to do this, is to use the `source` hook:
{
// the source hook requires a `listen` function, and a `get` function.
// let's skip the `listen` function for now, and just focus on the `getValue` function.
const listen = jest.fn(() => () => {})
// the `getValue` function should return the current value of the source.
const get = jest.fn(() => value)
const p = prism(() => {
return prism.source(listen, get) * 2
})
value = 1
// our prism is cold right now. let's see what happens when we read its value.
expect(p.getValue()).toBe(2)
// `get` will be called once, because we're reading the value of the source for the first time.
expect(get).toHaveBeenCalledTimes(1)
// and `listen` won't be called at all
expect(listen).toHaveBeenCalledTimes(0)
get.mockClear()
// now let's make the prism hot
const unsubscribe = p.onStale(() => {})
expect(p.isHot).toBe(true)
expect(p.getValue()).toBe(2)
// `get` will be called again, because we're reading the value of the source for the second time.
expect(get).toHaveBeenCalledTimes(1)
// and `listen` will be called once, because our prism wants to be notified when the source changes.
expect(listen).toHaveBeenCalledTimes(1)
get.mockClear()
listen.mockClear()
// now, since our `listen` function is a mock, it won't actually do anything,
// so the prism still won't know when the source changes.
value = 2
expect(p.getValue()).toBe(2)
// `get` won't be called again, because the source hasn't changed.
expect(get).toHaveBeenCalledTimes(0)
unsubscribe()
}
{
let value = 0
// now let's implement an actual `listen` function.
// first, we need to keep track of all the listeners that our source wil have
const listeners = new Set<(val: number) => void>()
// the `listen` function should return an stop function.
// the stop function should stop listening to the source.
const listen = jest.fn((fn) => {
listeners.add(fn)
return function stop() {
listeners.delete(fn)
}
})
const get = jest.fn(() => value)
// and now we need to define a function that will notify all the listeners that the source has changed.
const set = (newValue: number) => {
if (newValue !== value) {
value = newValue
listeners.forEach((fn) => fn(value))
}
}
// don't worry, there are helpers for this in dataverse. but for now, we'll implement
// it manually to understand how it works.
// now let's create a prism that depends on our source.
const p = prism(() => {
return prism.source(listen, get) * 2
})
// let's make the prism hot
const staleListener = jest.fn()
const unsubscribe = p.onStale(staleListener)
expect(p.isHot).toBe(true)
// and let's read its value
expect(p.getValue()).toBe(0)
// `get` will be called once, because we're reading the value of the source for the first time.
expect(get).toHaveBeenCalledTimes(1)
// and `listen` will be called once, because our prism wants to be notified when the source changes.
expect(listen).toHaveBeenCalledTimes(1)
get.mockClear()
listen.mockClear()
// now let's change the value of the source
set(1)
// this time, our prism will know that the source has changed, and it'll update its value.
expect(p.getValue()).toBe(2)
// and that's how we create a prism that depends on a changing value.
unsubscribe()
}
})
// in practice, we'll almost never need to use the `source` hook directly,
// and we'll never need to provide our own `listen` and `get` functions.
// instead, we'll use `Atom`s, which are sources that are already implemented for us.
test(`Using Atoms`, () => {})
})

View file

@ -4,7 +4,7 @@
* @packageDocumentation * @packageDocumentation
*/ */
export type {IdentityPrismProvider} from './Atom' export type {PointerToPrismProvider} from './Atom'
export {default as Atom, val, pointerToPrism} from './Atom' export {default as Atom, val, pointerToPrism} from './Atom'
export {isPrism} from './prism/Interface' export {isPrism} from './prism/Interface'
export type {Prism} from './prism/Interface' export type {Prism} from './prism/Interface'

View file

@ -44,7 +44,7 @@ export default class Sequence {
_playableRangeD: undefined | Prism<{start: number; end: number}> _playableRangeD: undefined | Prism<{start: number; end: number}>
readonly pointer: ISequence['pointer'] = pointer({root: this, path: []}) readonly pointer: ISequence['pointer'] = pointer({root: this, path: []})
readonly $$isIdentityPrismProvider = true readonly $$isPointerToPrismProvider = true
readonly _logger: ILogger readonly _logger: ILogger
constructor( constructor(
@ -81,7 +81,7 @@ export default class Sequence {
}) })
} }
getIdentityPrism(path: Array<string | number>): Prism<unknown> { pointerToPrism(path: Array<string | number>): Prism<unknown> {
if (path.length === 0) { if (path.length === 0) {
return prism((): ISequence['pointer']['$$__pointer_type'] => ({ return prism((): ISequence['pointer']['$$__pointer_type'] => ({
length: val(this.pointer.length), length: val(this.pointer.length),

View file

@ -14,7 +14,7 @@ import type {
SerializableValue, SerializableValue,
} from '@theatre/shared/utils/types' } from '@theatre/shared/utils/types'
import {valToAtom} from '@theatre/shared/utils/valToAtom' import {valToAtom} from '@theatre/shared/utils/valToAtom'
import type {IdentityPrismProvider, Prism, Pointer} from '@theatre/dataverse' import type {PointerToPrismProvider, Prism, Pointer} from '@theatre/dataverse'
import {Atom, getPointerParts, pointer, prism, val} from '@theatre/dataverse' import {Atom, getPointerParts, pointer, prism, val} from '@theatre/dataverse'
import type SheetObjectTemplate from './SheetObjectTemplate' import type SheetObjectTemplate from './SheetObjectTemplate'
@ -38,11 +38,11 @@ type SheetObjectPropsValue = SerializableMap
* Note that this cannot be generic over `Props`, since the user is * Note that this cannot be generic over `Props`, since the user is
* able to change prop configs for the sheet object's properties. * able to change prop configs for the sheet object's properties.
*/ */
export default class SheetObject implements IdentityPrismProvider { export default class SheetObject implements PointerToPrismProvider {
get type(): 'Theatre_SheetObject' { get type(): 'Theatre_SheetObject' {
return 'Theatre_SheetObject' return 'Theatre_SheetObject'
} }
readonly $$isIdentityPrismProvider: true = true readonly $$isPointerToPrismProvider: true = true
readonly address: SheetObjectAddress readonly address: SheetObjectAddress
readonly publicApi: TheatreSheetObject readonly publicApi: TheatreSheetObject
private readonly _initialValue = new Atom<SheetObjectPropsValue>({}) private readonly _initialValue = new Atom<SheetObjectPropsValue>({})
@ -211,7 +211,8 @@ export default class SheetObject implements IdentityPrismProvider {
) as SerializableValue as T ) as SerializableValue as T
} }
getIdentityPrism(path: Array<string | number>): Prism<unknown> { pointerToPrism<P>(pointer: Pointer<P>): Prism<P> {
const {path} = getPointerParts(pointer)
/** /**
* @remarks * @remarks
* TODO perf: Too much indirection here. * TODO perf: Too much indirection here.
@ -219,7 +220,7 @@ export default class SheetObject implements IdentityPrismProvider {
return prism(() => { return prism(() => {
const allValuesP = val(this.getValues()) const allValuesP = val(this.getValues())
return val(pointerDeep(allValuesP as $FixMe, path)) as SerializableMap return val(pointerDeep(allValuesP as $FixMe, path)) as SerializableMap
}) }) as $IntentionalAny as Prism<P>
} }
/** /**