Document dataverse API

This commit is contained in:
Andrew Prifer 2022-01-19 13:06:13 +01:00 committed by Aria
parent 5763f2bca4
commit 599cc101c9
17 changed files with 555 additions and 19 deletions

View file

@ -19,8 +19,19 @@ enum ValueTypes {
Other,
}
/**
* Interface for objects that can provide a derivation at a certain path.
*/
export interface IdentityDerivationProvider {
/**
* @internal
*/
readonly $$isIdentityDerivationProvider: true
/**
* Returns a derivation of the value at the provided path.
*
* @param path The path to create the derivation at.
*/
getIdentityDerivation(path: Array<string | number>): IDerivation<unknown>
}
@ -99,12 +110,24 @@ class Scope {
}
}
/**
* Wraps an object whose (sub)properties can be individually tracked.
*/
export default class Atom<State extends {}>
implements IdentityDerivationProvider
{
private _currentState: State
/**
* @internal
*/
readonly $$isIdentityDerivationProvider = true
private readonly _rootScope: Scope
/**
* Convenience property that gives you a pointer to the root of the atom.
*
* @remarks
* Equivalent to `pointer({ root: thisAtom, path: [] })`.
*/
readonly pointer: Pointer<State>
constructor(initialState: State) {
@ -113,6 +136,11 @@ export default class Atom<State extends {}>
this.pointer = pointer({root: this as $FixMe, path: []})
}
/**
* Sets the state of the atom.
*
* @param newState The new state of the atom.
*/
setState(newState: State) {
const oldState = this._currentState
this._currentState = newState
@ -120,14 +148,39 @@ export default class Atom<State extends {}>
this._checkUpdates(this._rootScope, oldState, newState)
}
/**
* Gets the current state of the atom.
*/
getState() {
return this._currentState
}
/**
* Gets the state of the atom at `path`.
*/
getIn(path: (string | number)[]): unknown {
return path.length === 0 ? this.getState() : get(this.getState(), path)
}
/**
* Creates a new state object from the current one, where the value at `path`
* is replaced by the return value of `reducer`, then sets it.
*
* @remarks
* Doesn't mutate the old state, and preserves referential equality between
* values of the old state and the new state where possible.
*
* @example
* ```ts
* someAtom.getIn(['a']) // 1
* someAtom.reduceState(['a'], (state) => state + 1);
* someAtom.getIn(['a']) // 2
* ```
*
* @param path The path to call the reducer at.
* @param reducer The function to use for creating the new state.
*/
// TODO: Why is this a property and not a method?
reduceState: PathBasedReducer<State, State> = (
path: $IntentionalAny[],
reducer: $IntentionalAny,
@ -137,6 +190,9 @@ export default class Atom<State extends {}>
return newState
}
/**
* Sets the state of the atom at `path`.
*/
setIn(path: $FixMe[], val: $FixMe) {
return this.reduceState(path, () => val)
}
@ -181,6 +237,11 @@ export default class Atom<State extends {}>
return untap
}
/**
* Returns a new derivation of the value at the provided path.
*
* @param path The path to create the derivation at.
*/
getIdentityDerivation(path: Array<string | number>): IDerivation<unknown> {
return new DerivationFromSource<$IntentionalAny>(
(listener) => this._onPathValueChange(path, listener),
@ -191,6 +252,12 @@ export default class Atom<State extends {}>
const identityDerivationWeakMap = new WeakMap<{}, IDerivation<unknown>>()
/**
* Returns a derivation of the value at the provided pointer. Derivations are
* cached per pointer.
*
* @param pointer The pointer to return the derivation at.
*/
export const valueDerivation = <P extends PointerType<$IntentionalAny>>(
pointer: P,
): IDerivation<P extends PointerType<infer T> ? T : void> => {
@ -211,6 +278,7 @@ export const valueDerivation = <P extends PointerType<$IntentionalAny>>(
return derivation as $IntentionalAny
}
// TODO: Rename it to isIdentityDerivationProvider
function isIdentityChangeProvider(
val: unknown,
): val is IdentityDerivationProvider {
@ -221,6 +289,16 @@ function isIdentityChangeProvider(
)
}
/**
* Convenience function that returns a plain value from its argument, whether it
* is a pointer, a derivation or a plain value itself.
*
* @remarks
* For pointers, the value is returned by first creating a derivation, so it is
* reactive e.g. when used in a `prism`.
*
* @param pointerOrDerivationOrPlainValue The argument to return a value from.
*/
export const val = <P>(
pointerOrDerivationOrPlainValue: P,
): P extends PointerType<infer T>

View file

@ -1,33 +1,88 @@
import DerivationFromSource from './derivations/DerivationFromSource'
import type {IDerivation} from './derivations/IDerivation'
import Emitter from './utils/Emitter'
/**
* Common interface for Box types. Boxes wrap a single value.
*/
export interface IBox<V> {
/**
* Sets the value of the Box.
*
* @param v The value to update the Box with.
*/
set(v: V): void
/**
* 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.
*
* @see derivation
*/
get(): V
/**
* Creates a derivation of the Box that you can use to track changes to it.
*/
derivation: IDerivation<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.
* 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.
*/
export default class Box<V> implements IBox<V> {
private _publicDerivation: IDerivation<V>
private _emitter = new Emitter<V>()
constructor(protected _value: V) {
/**
* @param _value The initial value of the Box.
*/
constructor(
/**
* @internal
*/
protected _value: V,
) {
this._publicDerivation = new DerivationFromSource(
(listener) => this._emitter.tappable.tap(listener),
this.get.bind(this),
)
}
/**
* Sets the value of the Box.
*
* @param v The value to update the Box with.
*/
set(v: V) {
if (v === this._value) return
this._value = v
this._emitter.emit(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.
*
* @see derivation
*/
get() {
return this._value
}
/**
* Creates a derivation of the Box that you can use to track changes to it.
*/
get derivation() {
return this._publicDerivation
}

View file

@ -6,11 +6,27 @@ import Box from './Box'
import type {$FixMe, $IntentionalAny} from './types'
import {valueDerivation} from './Atom'
/**
* Allows creating pointer-derivations where the pointer can be switched out.
*
* @remarks
* This allows reacting not just to value changes at a certain pointer, but changes
* to the proxied pointer too.
*/
export default class PointerProxy<O extends {}>
implements IdentityDerivationProvider
{
/**
* @internal
*/
readonly $$isIdentityDerivationProvider = true
private readonly _currentPointerBox: IBox<Pointer<O>>
/**
* Convenience pointer pointing to the root of this PointerProxy.
*
* @remarks
* Allows convenient use of {@link valueDerivation} and {@link val}.
*/
readonly pointer: Pointer<O>
constructor(currentPointer: Pointer<O>) {
@ -18,10 +34,19 @@ export default class PointerProxy<O extends {}>
this.pointer = pointer({root: this as $FixMe, path: []})
}
/**
* Sets the underlying pointer.
* @param p The pointer to be proxied.
*/
setPointer(p: Pointer<O>) {
this._currentPointerBox.set(p)
}
/**
* Returns a derivation of the value at the provided sub-path of the proxied pointer.
*
* @param path The path to create the derivation at.
*/
getIdentityDerivation(path: Array<string | number>) {
return this._currentPointerBox.derivation.flatMap((p) => {
const subPointer = path.reduce(

View file

@ -1,5 +1,9 @@
type ICallback = (t: number) => void
/**
* The Ticker class helps schedule callbacks. Scheduled callbacks are executed per tick. Ticks can be triggered by an
* external scheduling strategy, e.g. a raf.
*/
export default class Ticker {
private _scheduledForThisOrNextTick: Set<ICallback>
private _scheduledForNextTick: Set<ICallback>
@ -15,12 +19,16 @@ export default class Ticker {
/**
* Registers for fn to be called either on this tick or the next tick.
*
* If registerSideEffect() is called while Ticker.tick() is running, the
* If `onThisOrNextTick()` is called while `Ticker.tick()` is running, the
* side effect _will_ be called within the running tick. If you don't want this
* behavior, you can use registerSideEffectForNextTick().
* behavior, you can use `onNextTick()`.
*
* Note that fn will be added to a Set(). Which means, if you call registerSideEffect(fn)
* Note that `fn` will be added to a `Set()`. Which means, if you call `onThisOrNextTick(fn)`
* with the same fn twice in a single tick, it'll only run once.
*
* @param fn The function to be registered.
*
* @see offThisOrNextTick
*/
onThisOrNextTick(fn: ICallback) {
this._scheduledForThisOrNextTick.add(fn)
@ -29,26 +37,55 @@ export default class Ticker {
/**
* Registers a side effect to be called on the next tick.
*
* @see Ticker:onThisOrNextTick()
* @param fn The function to be registered.
*
* @see onThisOrNextTick
* @see offNextTick
*/
onNextTick(fn: ICallback) {
this._scheduledForNextTick.add(fn)
}
/**
* De-registers a fn to be called either on this tick or the next tick.
*
* @param fn The function to be de-registered.
*
* @see onThisOrNextTick
*/
offThisOrNextTick(fn: ICallback) {
this._scheduledForThisOrNextTick.delete(fn)
}
/**
* De-registers a fn to be called on the next tick.
*
* @param fn The function to be de-registered.
*
* @see onNextTick
*/
offNextTick(fn: ICallback) {
this._scheduledForNextTick.delete(fn)
}
/**
* The time at the start of the current tick if there is a tick in progress, otherwise defaults to
* `performance.now()`.
*/
get time() {
if (this._ticking) {
return this._timeAtCurrentTick
} else return performance.now()
}
/**
* Triggers a tick which starts executing the callbacks scheduled for this tick.
*
* @param t The time at the tick.
*
* @see onThisOrNextTick
* @see onNextTick
*/
tick(t: number = performance.now()) {
this._ticking = true
this._timeAtCurrentTick = t

View file

@ -12,58 +12,121 @@ import {
} from './prism/discoveryMechanism'
type IDependent = (msgComingFrom: IDerivation<$IntentionalAny>) => void
/**
* Represents a derivation whose changes can be tracked. To be used as the base class for all derivations.
*/
export default abstract class AbstractDerivation<V> implements IDerivation<V> {
/**
* Whether the object is a derivation.
*/
readonly isDerivation: true = true
private _didMarkDependentsAsStale: boolean = false
private _isHot: boolean = false
private _isFresh: boolean = false
/**
* @internal
*/
protected _lastValue: undefined | V = undefined
/**
* @internal
*/
protected _dependents: Set<IDependent> = new Set()
/**
* @internal
*/
protected _dependencies: Set<IDerivation<$IntentionalAny>> = new Set()
/**
* @internal
*/
protected abstract _recalculate(): V
/**
* @internal
*/
protected abstract _reactToDependencyBecomingStale(
which: IDerivation<unknown>,
): void
constructor() {}
/**
* Whether the derivation is hot.
*/
get isHot(): boolean {
return this._isHot
}
/**
* @internal
*/
protected _addDependency(d: IDerivation<$IntentionalAny>) {
if (this._dependencies.has(d)) return
this._dependencies.add(d)
if (this._isHot) d.addDependent(this._internal_markAsStale)
}
/**
* @internal
*/
protected _removeDependency(d: IDerivation<$IntentionalAny>) {
if (!this._dependencies.has(d)) return
this._dependencies.delete(d)
if (this._isHot) d.removeDependent(this._internal_markAsStale)
}
/**
* Returns a `Tappable` of the changes of this derivation.
*/
changes(ticker: Ticker): Tappable<V> {
return new DerivationEmitter(this, ticker).tappable()
}
/**
* Like {@link AbstractDerivation.changes} but with a different performance model. `changesWithoutValues` returns a `Tappable` that
* updates every time the derivation is updated, even if the value didn't change, and the callback is called without
* the value. The advantage of this is that you have control over when the derivation is freshened, it won't
* automatically be kept fresh.
*/
changesWithoutValues(): Tappable<void> {
return new DerivationValuelessEmitter(this).tappable()
}
/**
* Keep the derivation hot, even if there are no tappers (subscribers).
*/
keepHot() {
return this.changesWithoutValues().tap(() => {})
}
/**
* Convenience method that taps (subscribes to) the derivation using `this.changes(ticker).tap(fn)` and immediately calls
* the callback with the current value.
*
* @param ticker The ticker to use for batching.
* @param fn The callback to call on update.
*
* @see changes
*/
tapImmediate(ticker: Ticker, fn: (cb: V) => void): VoidFn {
const untap = this.changes(ticker).tap(fn)
fn(this.getValue())
return untap
}
/**
* Add a derivation as a dependent of this derivation.
*
* @param d The derivation to be made a dependent of this derivation.
*
* @see removeDependent
*/
// TODO: document this better, what are dependents?
addDependent(d: IDependent) {
const hadDepsBefore = this._dependents.size > 0
this._dependents.add(d)
@ -74,7 +137,11 @@ export default abstract class AbstractDerivation<V> implements IDerivation<V> {
}
/**
* @sealed
* Remove a derivation as a dependent of this derivation.
*
* @param d The derivation to be removed from as a dependent of this derivation.
*
* @see addDependent
*/
removeDependent(d: IDependent) {
const hadDepsBefore = this._dependents.size > 0
@ -89,6 +156,7 @@ export default abstract class AbstractDerivation<V> implements IDerivation<V> {
* This is meant to be called by subclasses
*
* @sealed
* @internal
*/
protected _markAsStale(which: IDerivation<$IntentionalAny>) {
this._internal_markAsStale(which)
@ -107,6 +175,9 @@ export default abstract class AbstractDerivation<V> implements IDerivation<V> {
})
}
/**
* Gets the current value of the derivation. If the value is stale, it causes the derivation to freshen.
*/
getValue(): V {
reportResolutionStart(this)
@ -144,14 +215,41 @@ export default abstract class AbstractDerivation<V> implements IDerivation<V> {
}
}
/**
* @internal
*/
protected _keepHot() {}
/**
* @internal
*/
protected _becomeCold() {}
/**
* Creates a new derivation from this derivation using the provided mapping function. The new derivation's value will be
* `fn(thisDerivation.getValue())`.
*
* @param fn The mapping function to use. Note: it accepts a plain value, not a derivation.
*/
map<T>(fn: (v: V) => T): IDerivation<T> {
return map(this, fn)
}
/**
* Same as {@link AbstractDerivation.map}, but the mapping function can also return a derivation, in which case the derivation returned
* by `flatMap` takes the value of that derivation.
*
* @example
* ```ts
* // Simply using map() here would return the inner derivation when we call getValue()
* new Box(3).derivation.map((value) => new Box(value).derivation).getValue()
*
* // Using flatMap() eliminates the inner derivation
* new Box(3).derivation.flatMap((value) => new Box(value).derivation).getValue()
* ```
*
* @param fn The mapping function to use. Note: it accepts a plain value, not a derivation.
*/
flatMap<R>(
fn: (v: V) => R,
): IDerivation<R extends IDerivation<infer T> ? T : R> {

View file

@ -1,17 +1,29 @@
import AbstractDerivation from './AbstractDerivation'
/**
* A derivation whose value never changes.
*/
export default class ConstantDerivation<V> extends AbstractDerivation<V> {
_v: V
private readonly _v: V
/**
* @param v The value of the derivation.
*/
constructor(v: V) {
super()
this._v = v
return this
}
/**
* @internal
*/
_recalculate() {
return this._v
}
/**
* @internal
*/
_reactToDependencyBecomingStale() {}
}

View file

@ -3,6 +3,9 @@ import Emitter from '../utils/Emitter'
import type {default as Tappable} from '../utils/Tappable'
import type {IDerivation} from './IDerivation'
/**
* An event emitter that emits events on changes to a derivation.
*/
export default class DerivationEmitter<V> {
private _derivation: IDerivation<V>
private _ticker: Ticker
@ -11,6 +14,10 @@ export default class DerivationEmitter<V> {
private _lastValueRecorded: boolean
private _hadTappers: boolean
/**
* @param derivation The derivation to emit events for.
* @param ticker The ticker to use to batch events.
*/
constructor(derivation: IDerivation<V>, ticker: Ticker) {
this._derivation = derivation
this._ticker = ticker
@ -40,6 +47,9 @@ export default class DerivationEmitter<V> {
}
}
/**
* The tappable associated with the emitter. You can use it to tap (subscribe to) the underlying derivation.
*/
tappable(): Tappable<V> {
return this._emitter.tappable
}

View file

@ -3,11 +3,18 @@ import AbstractDerivation from './AbstractDerivation'
const noop = () => {}
/**
* Represents a derivation based on a tappable (subscribable) data source.
*/
export default class DerivationFromSource<V> extends AbstractDerivation<V> {
private _untapFromChanges: () => void
private _cachedValue: undefined | V
private _hasCachedValue: boolean
/**
* @param _tapToSource A function that takes a listener and subscribes it to the underlying data source.
* @param _getValueFromSource A function that returns the current value of the data source.
*/
constructor(
private readonly _tapToSource: (listener: (newValue: V) => void) => VoidFn,
private readonly _getValueFromSource: () => V,
@ -18,6 +25,9 @@ export default class DerivationFromSource<V> extends AbstractDerivation<V> {
this._hasCachedValue = false
}
/**
* @internal
*/
_recalculate() {
if (this.isHot) {
if (!this._hasCachedValue) {
@ -30,6 +40,9 @@ export default class DerivationFromSource<V> extends AbstractDerivation<V> {
}
}
/**
* @internal
*/
_keepHot() {
this._hasCachedValue = false
this._cachedValue = undefined
@ -41,6 +54,9 @@ export default class DerivationFromSource<V> extends AbstractDerivation<V> {
})
}
/**
* @internal
*/
_becomeCold() {
this._untapFromChanges()
this._untapFromChanges = noop
@ -49,5 +65,8 @@ export default class DerivationFromSource<V> extends AbstractDerivation<V> {
this._cachedValue = undefined
}
/**
* @internal
*/
_reactToDependencyBecomingStale() {}
}

View file

@ -3,7 +3,10 @@ import type {default as Tappable} from '../utils/Tappable'
import type {IDerivation} from './IDerivation'
/**
* Just like DerivationEmitter, except it doesn't emit the value and doesn't need a ticker
* Like DerivationEmitter, but with a different performance model. DerivationValuelessEmitter emits every time the
* derivation is updated, even if the value didn't change, and tappers are called without the value. The advantage of
* this is that you have control over when the underlying derivation is freshened, it won't automatically be freshened
* by the emitter.
*/
export default class DerivationValuelessEmitter<V> {
_derivation: IDerivation<V>
@ -39,6 +42,9 @@ export default class DerivationValuelessEmitter<V> {
}
}
/**
* The tappable associated with the emitter. You can use it to tap (subscribe to) the underlying derivation.
*/
tappable(): Tappable<void> {
return this._emitter.tappable
}

View file

@ -3,26 +3,102 @@ import type {$IntentionalAny, VoidFn} from '../types'
import type Tappable from '../utils/Tappable'
type IDependent = (msgComingFrom: IDerivation<$IntentionalAny>) => void
/**
* Common interface for derivations.
*/
export interface IDerivation<V> {
/**
* Whether the object is a derivation.
*/
isDerivation: true
/**
* Whether the derivation is hot.
*/
isHot: boolean
/**
* Returns a `Tappable` of the changes of this derivation.
*/
changes(ticker: Ticker): Tappable<V>
/**
* Like {@link changes} but with a different performance model. `changesWithoutValues` returns a {@link Tappable} that
* updates every time the derivation is updated, even if the value didn't change, and the callback is called without
* the value. The advantage of this is that you have control over when the derivation is freshened, it won't
* automatically be kept fresh.
*/
changesWithoutValues(): Tappable<void>
/**
* Keep the derivation hot, even if there are no tappers (subscribers).
*/
keepHot(): VoidFn
/**
* Convenience method that taps (subscribes to) the derivation using `this.changes(ticker).tap(fn)` and immediately calls
* the callback with the current value.
*
* @param ticker The ticker to use for batching.
* @param fn The callback to call on update.
*
* @see changes
*/
tapImmediate(ticker: Ticker, fn: (cb: V) => void): VoidFn
/**
* Add a derivation as a dependent of this derivation.
*
* @param d The derivation to be made a dependent of this derivation.
*
* @see removeDependent
*/
addDependent(d: IDependent): void
/**
* Remove a derivation as a dependent of this derivation.
*
* @param d The derivation to be removed from as a dependent of this derivation.
*
* @see addDependent
*/
removeDependent(d: IDependent): void
/**
* Gets the current value of the derivation. If the value is stale, it causes the derivation to freshen.
*/
getValue(): V
/**
* Creates a new derivation from this derivation using the provided mapping function. The new derivation's value will be
* `fn(thisDerivation.getValue())`.
*
* @param fn The mapping function to use. Note: it accepts a plain value, not a derivation.
*/
map<T>(fn: (v: V) => T): IDerivation<T>
/**
* Same as {@link IDerivation.map}, but the mapping function can also return a derivation, in which case the derivation returned
* by `flatMap` takes the value of that derivation.
*
* @example
* // Simply using `map()` here would return the inner derivation when we call `getValue()`
* new Box(3).derivation.map((value) => new Box(value).derivation).getValue()
*
* // Using `flatMap()` eliminates the inner derivation
* new Box(3).derivation.flatMap((value) => new Box(value).derivation).getValue()
*
* @param fn The mapping function to use. Note: it accepts a plain value, not a derivation.
*/
flatMap<R>(
fn: (v: V) => R,
): IDerivation<R extends IDerivation<infer T> ? T : R>
}
/**
* Returns whether `d` is a derivation.
*/
export function isDerivation(d: any): d is IDerivation<unknown> {
return d && d.isDerivation && d.isDerivation === true
}

View file

@ -334,6 +334,12 @@ type IPrismFn = {
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)
}

View file

@ -1,3 +1,9 @@
/**
* The animation-optimized FRP library powering the internals of Theatre.js.
*
* @packageDocumentation
*/
export type {IdentityDerivationProvider} from './Atom'
export {default as Atom, val, valueDerivation} from './Atom'
export {default as Box} from './Box'

View file

@ -22,10 +22,19 @@ export type UnindexablePointer = {
const pointerMetaWeakMap = new WeakMap<{}, PointerMeta>()
/**
* A wrapper type for the type a `Pointer` points to.
*/
export type PointerType<O> = {
$$__pointer_type: O
}
/**
* The type of {@link Atom} pointers. See {@link pointer|pointer()} for an
* explanation of pointers.
*
* @see Atom
*/
export type Pointer<O> = PointerType<O> &
(O extends UnindexableTypesForPointer
? UnindexablePointer
@ -67,6 +76,12 @@ const handler = {
},
}
/**
* Returns the metadata associated with the pointer. Usually the root object and
* the path.
*
* @param p The pointer.
*/
export const getPointerMeta = (
p: Pointer<$IntentionalAny> | Pointer<{}> | Pointer<unknown>,
): PointerMeta => {
@ -77,6 +92,18 @@ export const getPointerMeta = (
return meta
}
/**
* Returns the root object and the path of the pointer.
*
* @example
* ```ts
* const {root, path} = getPointerParts(pointer)
* ```
*
* @param p The pointer.
*
* @returns An object with two properties: `root`-the root object or the pointer, and `path`-the path of the pointer. `path` is an array of the property-chain.
*/
export const getPointerParts = (
p: Pointer<$IntentionalAny> | Pointer<{}> | Pointer<unknown>,
): {root: {}; path: PathToProp} => {
@ -84,25 +111,52 @@ export const getPointerParts = (
return {root, path}
}
function pointer<O>({
root,
path,
}: {
root: {}
path: Array<string | number>
}): Pointer<O>
function pointer(args: {root: {}; path?: Array<string | number>}) {
/**
* Creates a pointer to a (nested) property of an {@link Atom}.
*
* @remarks
* Pointers are used to make derivations of properties or nested properties of
* {@link Atom|Atoms}.
*
* Pointers also allow easy construction of new pointers pointing to nested members
* of the root object, by simply using property chaining. E.g. `somePointer.a.b` will
* create a new pointer that has `'a'` and `'b'` added to the path of `somePointer`.
*
* @example
* ```ts
* // Here, sum is a derivation 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']}));
* });
*
* // Note, atoms have a convenience Atom.pointer property that points to the root,
* // which you would normally use in this situation.
* const sum = prism(() => {
* return val(someAtom.pointer.a) + val(someAtom.pointer.b);
* });
* ```
*
* @param args The pointer parameters.
* @param args.root The {@link Atom} the pointer applies to.
* @param args.path The path to the (nested) property the pointer points to.
*
* @typeParam O The type of the value being pointed to.
*/
function pointer<O>(args: {root: {}; path?: Array<string | number>}) {
const meta: PointerMeta = {
root: args.root as $IntentionalAny,
path: args.path ?? [],
}
const hiddenObj = {}
pointerMetaWeakMap.set(hiddenObj, meta)
return new Proxy(hiddenObj, handler) as Pointer<$IntentionalAny>
return new Proxy(hiddenObj, handler) as Pointer<O>
}
export default pointer
/**
* Returns whether `p` is a pointer.
*/
export const isPointer = (p: $IntentionalAny): p is Pointer<unknown> => {
return p && !!getPointerMeta(p)
}

View file

@ -3,12 +3,19 @@ import Tappable from './Tappable'
type Tapper<V> = (v: V) => void
type Untap = () => void
/**
* An event emitter. Emit events that others can tap (subscribe to).
*/
export default class Emitter<V> {
private _tappers: Map<any, (v: V) => void>
private _lastTapperId: number
readonly tappable: Tappable<V>
private _onNumberOfTappersChangeListener: undefined | ((n: number) => void)
/**
* The Tappable associated with this emitter. You can use this to tap (subscribe to) events emitted.
*/
readonly tappable: Tappable<V>
constructor() {
this._lastTapperId = 0
this._tappers = new Map()
@ -39,16 +46,37 @@ export default class Emitter<V> {
}
}
/**
* Emit a value.
*
* @param payload The value to be emitted.
*/
emit(payload: V) {
this._tappers.forEach((cb) => {
cb(payload)
})
}
/**
* Checks whether the emitter has tappers (subscribers).
*/
hasTappers() {
return this._tappers.size !== 0
}
/**
* Handler to execute when the number of tappers (subscribers) changes.
*
* @callback Emitter~numberOfTappersChangeHandler
*
* @param {number} n The current number of tappers (subscribers).
*/
/**
* Calls callback when the number of tappers (subscribers) changes.
*
* @param {Emitter~requestCallback} cb The function to be called.
*/
onNumberOfTappersChange(cb: (n: number) => void) {
this._onNumberOfTappersChangeListener = cb
}

View file

@ -7,6 +7,9 @@ interface IProps<V> {
type Listener<V> = ((v: V) => void) | (() => void)
/**
* Represents a data-source that can be tapped (subscribed to).
*/
export default class Tappable<V> {
private _props: IProps<V>
private _tappers: Map<number, {bivarianceHack(v: V): void}['bivarianceHack']>
@ -55,6 +58,11 @@ export default class Tappable<V> {
})
}
/**
* Tap (subscribe to) the data source.
*
* @param cb The callback to be called on a change.
*/
tap(cb: Listener<V>): Untap {
const tapperId = this._lastTapperId++
this._tappers.set(tapperId, cb)