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

@ -15,7 +15,7 @@
"test": "jest", "test": "jest",
"postinstall": "husky install", "postinstall": "husky install",
"release": "zx scripts/release.mjs", "release": "zx scripts/release.mjs",
"build:api-docs": "zx scripts/build-api-docs.mjs", "build:api-docs": "zx scripts/build-api-docs.mjs /Users/andrew/Projects/theatre-docs/docs/api",
"lint:all": "eslint . --ext ts,tsx --ignore-path=.gitignore --rulesdir ./devEnv/eslint/rules" "lint:all": "eslint . --ext ts,tsx --ignore-path=.gitignore --rulesdir ./devEnv/eslint/rules"
}, },
"lint-staged": { "lint-staged": {

View file

@ -19,8 +19,19 @@ enum ValueTypes {
Other, Other,
} }
/**
* Interface for objects that can provide a derivation at a certain path.
*/
export interface IdentityDerivationProvider { export interface IdentityDerivationProvider {
/**
* @internal
*/
readonly $$isIdentityDerivationProvider: true 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> 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 {}> export default class Atom<State extends {}>
implements IdentityDerivationProvider implements IdentityDerivationProvider
{ {
private _currentState: State private _currentState: State
/**
* @internal
*/
readonly $$isIdentityDerivationProvider = true readonly $$isIdentityDerivationProvider = true
private readonly _rootScope: Scope 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> readonly pointer: Pointer<State>
constructor(initialState: State) { constructor(initialState: State) {
@ -113,6 +136,11 @@ export default class Atom<State extends {}>
this.pointer = pointer({root: this as $FixMe, path: []}) 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) { setState(newState: State) {
const oldState = this._currentState const oldState = this._currentState
this._currentState = newState this._currentState = newState
@ -120,14 +148,39 @@ export default class Atom<State extends {}>
this._checkUpdates(this._rootScope, oldState, newState) this._checkUpdates(this._rootScope, oldState, newState)
} }
/**
* Gets the current state of the atom.
*/
getState() { getState() {
return this._currentState return this._currentState
} }
/**
* Gets the state of the atom at `path`.
*/
getIn(path: (string | number)[]): unknown { getIn(path: (string | number)[]): unknown {
return path.length === 0 ? this.getState() : get(this.getState(), path) 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> = ( reduceState: PathBasedReducer<State, State> = (
path: $IntentionalAny[], path: $IntentionalAny[],
reducer: $IntentionalAny, reducer: $IntentionalAny,
@ -137,6 +190,9 @@ export default class Atom<State extends {}>
return newState return newState
} }
/**
* Sets the state of the atom at `path`.
*/
setIn(path: $FixMe[], val: $FixMe) { setIn(path: $FixMe[], val: $FixMe) {
return this.reduceState(path, () => val) return this.reduceState(path, () => val)
} }
@ -181,6 +237,11 @@ export default class Atom<State extends {}>
return untap 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> { getIdentityDerivation(path: Array<string | number>): IDerivation<unknown> {
return new DerivationFromSource<$IntentionalAny>( return new DerivationFromSource<$IntentionalAny>(
(listener) => this._onPathValueChange(path, listener), (listener) => this._onPathValueChange(path, listener),
@ -191,6 +252,12 @@ export default class Atom<State extends {}>
const identityDerivationWeakMap = new WeakMap<{}, IDerivation<unknown>>() 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>>( export const valueDerivation = <P extends PointerType<$IntentionalAny>>(
pointer: P, pointer: P,
): IDerivation<P extends PointerType<infer T> ? T : void> => { ): IDerivation<P extends PointerType<infer T> ? T : void> => {
@ -211,6 +278,7 @@ export const valueDerivation = <P extends PointerType<$IntentionalAny>>(
return derivation as $IntentionalAny return derivation as $IntentionalAny
} }
// TODO: Rename it to isIdentityDerivationProvider
function isIdentityChangeProvider( function isIdentityChangeProvider(
val: unknown, val: unknown,
): val is IdentityDerivationProvider { ): 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>( export const val = <P>(
pointerOrDerivationOrPlainValue: P, pointerOrDerivationOrPlainValue: P,
): P extends PointerType<infer T> ): P extends PointerType<infer T>

View file

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

View file

@ -6,11 +6,27 @@ import Box from './Box'
import type {$FixMe, $IntentionalAny} from './types' import type {$FixMe, $IntentionalAny} from './types'
import {valueDerivation} from './Atom' 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 {}> export default class PointerProxy<O extends {}>
implements IdentityDerivationProvider implements IdentityDerivationProvider
{ {
/**
* @internal
*/
readonly $$isIdentityDerivationProvider = true readonly $$isIdentityDerivationProvider = true
private readonly _currentPointerBox: IBox<Pointer<O>> 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> readonly pointer: Pointer<O>
constructor(currentPointer: Pointer<O>) { constructor(currentPointer: Pointer<O>) {
@ -18,10 +34,19 @@ export default class PointerProxy<O extends {}>
this.pointer = pointer({root: this as $FixMe, path: []}) this.pointer = pointer({root: this as $FixMe, path: []})
} }
/**
* Sets the underlying pointer.
* @param p The pointer to be proxied.
*/
setPointer(p: Pointer<O>) { setPointer(p: Pointer<O>) {
this._currentPointerBox.set(p) 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>) { getIdentityDerivation(path: Array<string | number>) {
return this._currentPointerBox.derivation.flatMap((p) => { return this._currentPointerBox.derivation.flatMap((p) => {
const subPointer = path.reduce( const subPointer = path.reduce(

View file

@ -1,5 +1,9 @@
type ICallback = (t: number) => void 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 { export default class Ticker {
private _scheduledForThisOrNextTick: Set<ICallback> private _scheduledForThisOrNextTick: Set<ICallback>
private _scheduledForNextTick: 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. * 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 * 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. * 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) { onThisOrNextTick(fn: ICallback) {
this._scheduledForThisOrNextTick.add(fn) this._scheduledForThisOrNextTick.add(fn)
@ -29,26 +37,55 @@ export default class Ticker {
/** /**
* Registers a side effect to be called on the next tick. * 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) { onNextTick(fn: ICallback) {
this._scheduledForNextTick.add(fn) 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) { offThisOrNextTick(fn: ICallback) {
this._scheduledForThisOrNextTick.delete(fn) 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) { offNextTick(fn: ICallback) {
this._scheduledForNextTick.delete(fn) 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() { get time() {
if (this._ticking) { if (this._ticking) {
return this._timeAtCurrentTick return this._timeAtCurrentTick
} else return performance.now() } 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()) { tick(t: number = performance.now()) {
this._ticking = true this._ticking = true
this._timeAtCurrentTick = t this._timeAtCurrentTick = t

View file

@ -12,58 +12,121 @@ import {
} from './prism/discoveryMechanism' } from './prism/discoveryMechanism'
type IDependent = (msgComingFrom: IDerivation<$IntentionalAny>) => void 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> { export default abstract class AbstractDerivation<V> implements IDerivation<V> {
/**
* Whether the object is a derivation.
*/
readonly isDerivation: true = true readonly isDerivation: true = true
private _didMarkDependentsAsStale: boolean = false private _didMarkDependentsAsStale: boolean = false
private _isHot: boolean = false private _isHot: boolean = false
private _isFresh: boolean = false private _isFresh: boolean = false
/**
* @internal
*/
protected _lastValue: undefined | V = undefined protected _lastValue: undefined | V = undefined
/**
* @internal
*/
protected _dependents: Set<IDependent> = new Set() protected _dependents: Set<IDependent> = new Set()
/**
* @internal
*/
protected _dependencies: Set<IDerivation<$IntentionalAny>> = new Set() protected _dependencies: Set<IDerivation<$IntentionalAny>> = new Set()
/**
* @internal
*/
protected abstract _recalculate(): V protected abstract _recalculate(): V
/**
* @internal
*/
protected abstract _reactToDependencyBecomingStale( protected abstract _reactToDependencyBecomingStale(
which: IDerivation<unknown>, which: IDerivation<unknown>,
): void ): void
constructor() {} constructor() {}
/**
* Whether the derivation is hot.
*/
get isHot(): boolean { get isHot(): boolean {
return this._isHot return this._isHot
} }
/**
* @internal
*/
protected _addDependency(d: IDerivation<$IntentionalAny>) { protected _addDependency(d: IDerivation<$IntentionalAny>) {
if (this._dependencies.has(d)) return if (this._dependencies.has(d)) return
this._dependencies.add(d) this._dependencies.add(d)
if (this._isHot) d.addDependent(this._internal_markAsStale) if (this._isHot) d.addDependent(this._internal_markAsStale)
} }
/**
* @internal
*/
protected _removeDependency(d: IDerivation<$IntentionalAny>) { protected _removeDependency(d: IDerivation<$IntentionalAny>) {
if (!this._dependencies.has(d)) return if (!this._dependencies.has(d)) return
this._dependencies.delete(d) this._dependencies.delete(d)
if (this._isHot) d.removeDependent(this._internal_markAsStale) if (this._isHot) d.removeDependent(this._internal_markAsStale)
} }
/**
* Returns a `Tappable` of the changes of this derivation.
*/
changes(ticker: Ticker): Tappable<V> { changes(ticker: Ticker): Tappable<V> {
return new DerivationEmitter(this, ticker).tappable() 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> { changesWithoutValues(): Tappable<void> {
return new DerivationValuelessEmitter(this).tappable() return new DerivationValuelessEmitter(this).tappable()
} }
/**
* Keep the derivation hot, even if there are no tappers (subscribers).
*/
keepHot() { keepHot() {
return this.changesWithoutValues().tap(() => {}) 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 { tapImmediate(ticker: Ticker, fn: (cb: V) => void): VoidFn {
const untap = this.changes(ticker).tap(fn) const untap = this.changes(ticker).tap(fn)
fn(this.getValue()) fn(this.getValue())
return untap 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) { addDependent(d: IDependent) {
const hadDepsBefore = this._dependents.size > 0 const hadDepsBefore = this._dependents.size > 0
this._dependents.add(d) 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) { removeDependent(d: IDependent) {
const hadDepsBefore = this._dependents.size > 0 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 * This is meant to be called by subclasses
* *
* @sealed * @sealed
* @internal
*/ */
protected _markAsStale(which: IDerivation<$IntentionalAny>) { protected _markAsStale(which: IDerivation<$IntentionalAny>) {
this._internal_markAsStale(which) 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 { getValue(): V {
reportResolutionStart(this) reportResolutionStart(this)
@ -144,14 +215,41 @@ export default abstract class AbstractDerivation<V> implements IDerivation<V> {
} }
} }
/**
* @internal
*/
protected _keepHot() {} protected _keepHot() {}
/**
* @internal
*/
protected _becomeCold() {} 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> { map<T>(fn: (v: V) => T): IDerivation<T> {
return map(this, fn) 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>( flatMap<R>(
fn: (v: V) => R, fn: (v: V) => R,
): IDerivation<R extends IDerivation<infer T> ? T : R> { ): IDerivation<R extends IDerivation<infer T> ? T : R> {

View file

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

View file

@ -3,6 +3,9 @@ import Emitter from '../utils/Emitter'
import type {default as Tappable} from '../utils/Tappable' import type {default as Tappable} from '../utils/Tappable'
import type {IDerivation} from './IDerivation' import type {IDerivation} from './IDerivation'
/**
* An event emitter that emits events on changes to a derivation.
*/
export default class DerivationEmitter<V> { export default class DerivationEmitter<V> {
private _derivation: IDerivation<V> private _derivation: IDerivation<V>
private _ticker: Ticker private _ticker: Ticker
@ -11,6 +14,10 @@ export default class DerivationEmitter<V> {
private _lastValueRecorded: boolean private _lastValueRecorded: boolean
private _hadTappers: 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) { constructor(derivation: IDerivation<V>, ticker: Ticker) {
this._derivation = derivation this._derivation = derivation
this._ticker = ticker 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> { tappable(): Tappable<V> {
return this._emitter.tappable return this._emitter.tappable
} }

View file

@ -3,11 +3,18 @@ import AbstractDerivation from './AbstractDerivation'
const noop = () => {} const noop = () => {}
/**
* Represents a derivation based on a tappable (subscribable) data source.
*/
export default class DerivationFromSource<V> extends AbstractDerivation<V> { export default class DerivationFromSource<V> extends AbstractDerivation<V> {
private _untapFromChanges: () => void private _untapFromChanges: () => void
private _cachedValue: undefined | V private _cachedValue: undefined | V
private _hasCachedValue: boolean 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( constructor(
private readonly _tapToSource: (listener: (newValue: V) => void) => VoidFn, private readonly _tapToSource: (listener: (newValue: V) => void) => VoidFn,
private readonly _getValueFromSource: () => V, private readonly _getValueFromSource: () => V,
@ -18,6 +25,9 @@ export default class DerivationFromSource<V> extends AbstractDerivation<V> {
this._hasCachedValue = false this._hasCachedValue = false
} }
/**
* @internal
*/
_recalculate() { _recalculate() {
if (this.isHot) { if (this.isHot) {
if (!this._hasCachedValue) { if (!this._hasCachedValue) {
@ -30,6 +40,9 @@ export default class DerivationFromSource<V> extends AbstractDerivation<V> {
} }
} }
/**
* @internal
*/
_keepHot() { _keepHot() {
this._hasCachedValue = false this._hasCachedValue = false
this._cachedValue = undefined this._cachedValue = undefined
@ -41,6 +54,9 @@ export default class DerivationFromSource<V> extends AbstractDerivation<V> {
}) })
} }
/**
* @internal
*/
_becomeCold() { _becomeCold() {
this._untapFromChanges() this._untapFromChanges()
this._untapFromChanges = noop this._untapFromChanges = noop
@ -49,5 +65,8 @@ export default class DerivationFromSource<V> extends AbstractDerivation<V> {
this._cachedValue = undefined this._cachedValue = undefined
} }
/**
* @internal
*/
_reactToDependencyBecomingStale() {} _reactToDependencyBecomingStale() {}
} }

View file

@ -3,7 +3,10 @@ import type {default as Tappable} from '../utils/Tappable'
import type {IDerivation} from './IDerivation' 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> { export default class DerivationValuelessEmitter<V> {
_derivation: IDerivation<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> { tappable(): Tappable<void> {
return this._emitter.tappable return this._emitter.tappable
} }

View file

@ -3,26 +3,102 @@ import type {$IntentionalAny, VoidFn} from '../types'
import type Tappable from '../utils/Tappable' import type Tappable from '../utils/Tappable'
type IDependent = (msgComingFrom: IDerivation<$IntentionalAny>) => void type IDependent = (msgComingFrom: IDerivation<$IntentionalAny>) => void
/**
* Common interface for derivations.
*/
export interface IDerivation<V> { export interface IDerivation<V> {
/**
* Whether the object is a derivation.
*/
isDerivation: true isDerivation: true
/**
* Whether the derivation is hot.
*/
isHot: boolean isHot: boolean
/**
* Returns a `Tappable` of the changes of this derivation.
*/
changes(ticker: Ticker): Tappable<V> 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> changesWithoutValues(): Tappable<void>
/**
* Keep the derivation hot, even if there are no tappers (subscribers).
*/
keepHot(): VoidFn 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 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 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 removeDependent(d: IDependent): void
/**
* Gets the current value of the derivation. If the value is stale, it causes the derivation to freshen.
*/
getValue(): V 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> 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>( flatMap<R>(
fn: (v: V) => R, fn: (v: V) => R,
): IDerivation<R extends IDerivation<infer T> ? T : R> ): IDerivation<R extends IDerivation<infer T> ? T : R>
} }
/**
* Returns whether `d` is a derivation.
*/
export function isDerivation(d: any): d is IDerivation<unknown> { export function isDerivation(d: any): d is IDerivation<unknown> {
return d && d.isDerivation && d.isDerivation === true return d && d.isDerivation && d.isDerivation === true
} }

View file

@ -334,6 +334,12 @@ type IPrismFn = {
inPrism: typeof inPrism 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) => { const prism: IPrismFn = (fn) => {
return new PrismDerivation(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 type {IdentityDerivationProvider} from './Atom'
export {default as Atom, val, valueDerivation} from './Atom' export {default as Atom, val, valueDerivation} from './Atom'
export {default as Box} from './Box' export {default as Box} from './Box'

View file

@ -22,10 +22,19 @@ export type UnindexablePointer = {
const pointerMetaWeakMap = new WeakMap<{}, PointerMeta>() const pointerMetaWeakMap = new WeakMap<{}, PointerMeta>()
/**
* A wrapper type for the type a `Pointer` points to.
*/
export type PointerType<O> = { export type PointerType<O> = {
$$__pointer_type: 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> & export type Pointer<O> = PointerType<O> &
(O extends UnindexableTypesForPointer (O extends UnindexableTypesForPointer
? UnindexablePointer ? 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 = ( export const getPointerMeta = (
p: Pointer<$IntentionalAny> | Pointer<{}> | Pointer<unknown>, p: Pointer<$IntentionalAny> | Pointer<{}> | Pointer<unknown>,
): PointerMeta => { ): PointerMeta => {
@ -77,6 +92,18 @@ export const getPointerMeta = (
return meta 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 = ( export const getPointerParts = (
p: Pointer<$IntentionalAny> | Pointer<{}> | Pointer<unknown>, p: Pointer<$IntentionalAny> | Pointer<{}> | Pointer<unknown>,
): {root: {}; path: PathToProp} => { ): {root: {}; path: PathToProp} => {
@ -84,25 +111,52 @@ export const getPointerParts = (
return {root, path} return {root, path}
} }
function pointer<O>({ /**
root, * Creates a pointer to a (nested) property of an {@link Atom}.
path, *
}: { * @remarks
root: {} * Pointers are used to make derivations of properties or nested properties of
path: Array<string | number> * {@link Atom|Atoms}.
}): Pointer<O> *
function pointer(args: {root: {}; path?: Array<string | number>}) { * 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 = { const meta: PointerMeta = {
root: args.root as $IntentionalAny, root: args.root as $IntentionalAny,
path: args.path ?? [], path: args.path ?? [],
} }
const hiddenObj = {} const hiddenObj = {}
pointerMetaWeakMap.set(hiddenObj, meta) pointerMetaWeakMap.set(hiddenObj, meta)
return new Proxy(hiddenObj, handler) as Pointer<$IntentionalAny> return new Proxy(hiddenObj, handler) as Pointer<O>
} }
export default pointer export default pointer
/**
* Returns whether `p` is a pointer.
*/
export const isPointer = (p: $IntentionalAny): p is Pointer<unknown> => { export const isPointer = (p: $IntentionalAny): p is Pointer<unknown> => {
return p && !!getPointerMeta(p) return p && !!getPointerMeta(p)
} }

View file

@ -3,12 +3,19 @@ import Tappable from './Tappable'
type Tapper<V> = (v: V) => void type Tapper<V> = (v: V) => void
type Untap = () => void type Untap = () => void
/**
* An event emitter. Emit events that others can tap (subscribe to).
*/
export default class Emitter<V> { export default class Emitter<V> {
private _tappers: Map<any, (v: V) => void> private _tappers: Map<any, (v: V) => void>
private _lastTapperId: number private _lastTapperId: number
readonly tappable: Tappable<V>
private _onNumberOfTappersChangeListener: undefined | ((n: number) => void) 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() { constructor() {
this._lastTapperId = 0 this._lastTapperId = 0
this._tappers = new Map() 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) { emit(payload: V) {
this._tappers.forEach((cb) => { this._tappers.forEach((cb) => {
cb(payload) cb(payload)
}) })
} }
/**
* Checks whether the emitter has tappers (subscribers).
*/
hasTappers() { hasTappers() {
return this._tappers.size !== 0 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) { onNumberOfTappersChange(cb: (n: number) => void) {
this._onNumberOfTappersChangeListener = cb this._onNumberOfTappersChangeListener = cb
} }

View file

@ -7,6 +7,9 @@ interface IProps<V> {
type Listener<V> = ((v: V) => void) | (() => void) type Listener<V> = ((v: V) => void) | (() => void)
/**
* Represents a data-source that can be tapped (subscribed to).
*/
export default class Tappable<V> { export default class Tappable<V> {
private _props: IProps<V> private _props: IProps<V>
private _tappers: Map<number, {bivarianceHack(v: V): void}['bivarianceHack']> 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 { tap(cb: Listener<V>): Untap {
const tapperId = this._lastTapperId++ const tapperId = this._lastTapperId++
this._tappers.set(tapperId, cb) this._tappers.set(tapperId, cb)

View file

@ -54,5 +54,23 @@ import {parse as parseJsonC} from 'jsonc-parser'
(pkg) => $`yarn workspace ${pkg} run build:api-json`, (pkg) => $`yarn workspace ${pkg} run build:api-json`,
), ),
) )
/*
We replace the Pointer name with a similar-looking one so that that shitty
api-documenter generates two different pages for Pointer and pointer, instead
of overwriting one with the other.
Apparently any non-english character, including this one will break links,
probably due to some overzealous regex. Didn't find any replacement that doesn't
change the look of the name AND doesn't break links, however the below code does
replace links too, in case we find something in the future. For now, we shouldn't
@link to the Pointer type in TSDoc comments.
*/
const replacement = 'Pointer\u200E'
fs.writeFileSync('./.temp/api/dataverse.api.json', fs.readFileSync('./.temp/api/dataverse.api.json', {
encoding: 'utf-8',
}).replaceAll('"name": "Pointer"', `"name": "${replacement}"`).replaceAll('{@link Pointer}', `{@link ${replacement}}`))
await $`api-documenter markdown --input-folder ${pathToApiJsonFiles} --output-folder ${outputPath}` await $`api-documenter markdown --input-folder ${pathToApiJsonFiles} --output-folder ${outputPath}`
})() })()