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",
"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": {

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)

View file

@ -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}`
})()