Document dataverse API
This commit is contained in:
parent
5763f2bca4
commit
599cc101c9
17 changed files with 555 additions and 19 deletions
|
@ -15,7 +15,7 @@
|
|||
"test": "jest",
|
||||
"postinstall": "husky install",
|
||||
"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-staged": {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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() {}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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() {}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -54,5 +54,23 @@ import {parse as parseJsonC} from 'jsonc-parser'
|
|||
(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}`
|
||||
})()
|
||||
|
|
Loading…
Reference in a new issue