diff --git a/package.json b/package.json index 9e2dbdb..960a702 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/packages/dataverse/src/Atom.ts b/packages/dataverse/src/Atom.ts index 698ec6a..4625ff7 100644 --- a/packages/dataverse/src/Atom.ts +++ b/packages/dataverse/src/Atom.ts @@ -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): IDerivation } @@ -99,12 +110,24 @@ class Scope { } } +/** + * Wraps an object whose (sub)properties can be individually tracked. + */ export default class Atom 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 constructor(initialState: State) { @@ -113,6 +136,11 @@ export default class Atom 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 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 = ( path: $IntentionalAny[], reducer: $IntentionalAny, @@ -137,6 +190,9 @@ export default class Atom 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 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): IDerivation { return new DerivationFromSource<$IntentionalAny>( (listener) => this._onPathValueChange(path, listener), @@ -191,6 +252,12 @@ export default class Atom const identityDerivationWeakMap = new WeakMap<{}, IDerivation>() +/** + * 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 =

>( pointer: P, ): IDerivation

? T : void> => { @@ -211,6 +278,7 @@ export const valueDerivation =

>( 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 =

( pointerOrDerivationOrPlainValue: P, ): P extends PointerType diff --git a/packages/dataverse/src/Box.ts b/packages/dataverse/src/Box.ts index 90c380f..9b334d0 100644 --- a/packages/dataverse/src/Box.ts +++ b/packages/dataverse/src/Box.ts @@ -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 { + /** + * 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 } +/** + * 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 implements IBox { private _publicDerivation: IDerivation private _emitter = new Emitter() - 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 } diff --git a/packages/dataverse/src/PointerProxy.ts b/packages/dataverse/src/PointerProxy.ts index f1c7ee7..7230e16 100644 --- a/packages/dataverse/src/PointerProxy.ts +++ b/packages/dataverse/src/PointerProxy.ts @@ -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 implements IdentityDerivationProvider { + /** + * @internal + */ readonly $$isIdentityDerivationProvider = true private readonly _currentPointerBox: IBox> + /** + * Convenience pointer pointing to the root of this PointerProxy. + * + * @remarks + * Allows convenient use of {@link valueDerivation} and {@link val}. + */ readonly pointer: Pointer constructor(currentPointer: Pointer) { @@ -18,10 +34,19 @@ export default class PointerProxy this.pointer = pointer({root: this as $FixMe, path: []}) } + /** + * Sets the underlying pointer. + * @param p The pointer to be proxied. + */ setPointer(p: Pointer) { 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) { return this._currentPointerBox.derivation.flatMap((p) => { const subPointer = path.reduce( diff --git a/packages/dataverse/src/Ticker.ts b/packages/dataverse/src/Ticker.ts index b890138..78de984 100644 --- a/packages/dataverse/src/Ticker.ts +++ b/packages/dataverse/src/Ticker.ts @@ -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 private _scheduledForNextTick: Set @@ -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 diff --git a/packages/dataverse/src/derivations/AbstractDerivation.ts b/packages/dataverse/src/derivations/AbstractDerivation.ts index 955e461..0e37dc4 100644 --- a/packages/dataverse/src/derivations/AbstractDerivation.ts +++ b/packages/dataverse/src/derivations/AbstractDerivation.ts @@ -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 implements IDerivation { + /** + * 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 = new Set() + + /** + * @internal + */ protected _dependencies: Set> = new Set() + /** + * @internal + */ protected abstract _recalculate(): V + + /** + * @internal + */ protected abstract _reactToDependencyBecomingStale( which: IDerivation, ): 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 { 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 { 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 implements IDerivation { } /** - * @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 implements IDerivation { * 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 implements IDerivation { }) } + /** + * 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 implements IDerivation { } } + /** + * @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(fn: (v: V) => T): IDerivation { 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( fn: (v: V) => R, ): IDerivation ? T : R> { diff --git a/packages/dataverse/src/derivations/ConstantDerivation.ts b/packages/dataverse/src/derivations/ConstantDerivation.ts index 8906332..97de197 100644 --- a/packages/dataverse/src/derivations/ConstantDerivation.ts +++ b/packages/dataverse/src/derivations/ConstantDerivation.ts @@ -1,17 +1,29 @@ import AbstractDerivation from './AbstractDerivation' +/** + * A derivation whose value never changes. + */ export default class ConstantDerivation extends AbstractDerivation { - _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() {} } diff --git a/packages/dataverse/src/derivations/DerivationEmitter.ts b/packages/dataverse/src/derivations/DerivationEmitter.ts index 50beda8..3af768b 100644 --- a/packages/dataverse/src/derivations/DerivationEmitter.ts +++ b/packages/dataverse/src/derivations/DerivationEmitter.ts @@ -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 { private _derivation: IDerivation private _ticker: Ticker @@ -11,6 +14,10 @@ export default class DerivationEmitter { 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, ticker: Ticker) { this._derivation = derivation this._ticker = ticker @@ -40,6 +47,9 @@ export default class DerivationEmitter { } } + /** + * The tappable associated with the emitter. You can use it to tap (subscribe to) the underlying derivation. + */ tappable(): Tappable { return this._emitter.tappable } diff --git a/packages/dataverse/src/derivations/DerivationFromSource.ts b/packages/dataverse/src/derivations/DerivationFromSource.ts index af8e8a2..d981a57 100644 --- a/packages/dataverse/src/derivations/DerivationFromSource.ts +++ b/packages/dataverse/src/derivations/DerivationFromSource.ts @@ -3,11 +3,18 @@ import AbstractDerivation from './AbstractDerivation' const noop = () => {} +/** + * Represents a derivation based on a tappable (subscribable) data source. + */ export default class DerivationFromSource extends AbstractDerivation { 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 extends AbstractDerivation { this._hasCachedValue = false } + /** + * @internal + */ _recalculate() { if (this.isHot) { if (!this._hasCachedValue) { @@ -30,6 +40,9 @@ export default class DerivationFromSource extends AbstractDerivation { } } + /** + * @internal + */ _keepHot() { this._hasCachedValue = false this._cachedValue = undefined @@ -41,6 +54,9 @@ export default class DerivationFromSource extends AbstractDerivation { }) } + /** + * @internal + */ _becomeCold() { this._untapFromChanges() this._untapFromChanges = noop @@ -49,5 +65,8 @@ export default class DerivationFromSource extends AbstractDerivation { this._cachedValue = undefined } + /** + * @internal + */ _reactToDependencyBecomingStale() {} } diff --git a/packages/dataverse/src/derivations/DerivationValuelessEmitter.ts b/packages/dataverse/src/derivations/DerivationValuelessEmitter.ts index be5f93b..45d8c9b 100644 --- a/packages/dataverse/src/derivations/DerivationValuelessEmitter.ts +++ b/packages/dataverse/src/derivations/DerivationValuelessEmitter.ts @@ -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 { _derivation: IDerivation @@ -39,6 +42,9 @@ export default class DerivationValuelessEmitter { } } + /** + * The tappable associated with the emitter. You can use it to tap (subscribe to) the underlying derivation. + */ tappable(): Tappable { return this._emitter.tappable } diff --git a/packages/dataverse/src/derivations/IDerivation.ts b/packages/dataverse/src/derivations/IDerivation.ts index b1e75be..05915bd 100644 --- a/packages/dataverse/src/derivations/IDerivation.ts +++ b/packages/dataverse/src/derivations/IDerivation.ts @@ -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 { + /** + * 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 + /** + * 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 + + /** + * 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(fn: (v: V) => T): IDerivation + /** + * 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( fn: (v: V) => R, ): IDerivation ? T : R> } +/** + * Returns whether `d` is a derivation. + */ export function isDerivation(d: any): d is IDerivation { return d && d.isDerivation && d.isDerivation === true } diff --git a/packages/dataverse/src/derivations/prism/prism.ts b/packages/dataverse/src/derivations/prism/prism.ts index 04d762b..666f4f7 100644 --- a/packages/dataverse/src/derivations/prism/prism.ts +++ b/packages/dataverse/src/derivations/prism/prism.ts @@ -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) } diff --git a/packages/dataverse/src/index.ts b/packages/dataverse/src/index.ts index 852c4db..383a638 100644 --- a/packages/dataverse/src/index.ts +++ b/packages/dataverse/src/index.ts @@ -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' diff --git a/packages/dataverse/src/pointer.ts b/packages/dataverse/src/pointer.ts index ba5c813..f1f002a 100644 --- a/packages/dataverse/src/pointer.ts +++ b/packages/dataverse/src/pointer.ts @@ -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 = { $$__pointer_type: O } +/** + * The type of {@link Atom} pointers. See {@link pointer|pointer()} for an + * explanation of pointers. + * + * @see Atom + */ export type Pointer = PointerType & (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, ): 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, ): {root: {}; path: PathToProp} => { @@ -84,25 +111,52 @@ export const getPointerParts = ( return {root, path} } -function pointer({ - root, - path, -}: { - root: {} - path: Array -}): Pointer -function pointer(args: {root: {}; path?: Array}) { +/** + * 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(args: {root: {}; path?: Array}) { 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 } export default pointer +/** + * Returns whether `p` is a pointer. + */ export const isPointer = (p: $IntentionalAny): p is Pointer => { return p && !!getPointerMeta(p) } diff --git a/packages/dataverse/src/utils/Emitter.ts b/packages/dataverse/src/utils/Emitter.ts index d062473..3db8cb3 100644 --- a/packages/dataverse/src/utils/Emitter.ts +++ b/packages/dataverse/src/utils/Emitter.ts @@ -3,12 +3,19 @@ import Tappable from './Tappable' type Tapper = (v: V) => void type Untap = () => void +/** + * An event emitter. Emit events that others can tap (subscribe to). + */ export default class Emitter { private _tappers: Map void> private _lastTapperId: number - readonly tappable: Tappable 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 + constructor() { this._lastTapperId = 0 this._tappers = new Map() @@ -39,16 +46,37 @@ export default class Emitter { } } + /** + * 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 } diff --git a/packages/dataverse/src/utils/Tappable.ts b/packages/dataverse/src/utils/Tappable.ts index 3bbc347..3201f52 100644 --- a/packages/dataverse/src/utils/Tappable.ts +++ b/packages/dataverse/src/utils/Tappable.ts @@ -7,6 +7,9 @@ interface IProps { type Listener = ((v: V) => void) | (() => void) +/** + * Represents a data-source that can be tapped (subscribed to). + */ export default class Tappable { private _props: IProps private _tappers: Map @@ -55,6 +58,11 @@ export default class Tappable { }) } + /** + * Tap (subscribe to) the data source. + * + * @param cb The callback to be called on a change. + */ tap(cb: Listener): Untap { const tapperId = this._lastTapperId++ this._tappers.set(tapperId, cb) diff --git a/scripts/build-api-docs.mjs b/scripts/build-api-docs.mjs index 28ff85b..71dca6c 100644 --- a/scripts/build-api-docs.mjs +++ b/scripts/build-api-docs.mjs @@ -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}` })()