theatre/packages/dataverse/src/derivations/AbstractDerivation.ts

284 lines
8.4 KiB
TypeScript
Raw Normal View History

2021-06-18 13:05:06 +02:00
import type Ticker from '../Ticker'
import type {$IntentionalAny, VoidFn} from '../types'
import type Tappable from '../utils/Tappable'
import DerivationEmitter from './DerivationEmitter'
import DerivationValuelessEmitter from './DerivationValuelessEmitter'
import flatMap from './flatMap'
import type {IDerivation} from './IDerivation'
import map from './map'
import {
reportResolutionEnd,
reportResolutionStart,
} from './prism/discoveryMechanism'
type IDependent = (msgComingFrom: IDerivation<$IntentionalAny>) => void
2022-01-19 13:06:13 +01:00
/**
* Represents a derivation whose changes can be tracked. To be used as the base class for all derivations.
*/
2021-06-18 13:05:06 +02:00
export default abstract class AbstractDerivation<V> implements IDerivation<V> {
2022-01-19 13:06:13 +01:00
/**
* Whether the object is a derivation.
*/
2021-06-18 13:05:06 +02:00
readonly isDerivation: true = true
private _didMarkDependentsAsStale: boolean = false
private _isHot: boolean = false
private _isFresh: boolean = false
2022-01-19 13:06:13 +01:00
/**
* @internal
*/
2021-06-18 13:05:06 +02:00
protected _lastValue: undefined | V = undefined
2022-01-19 13:06:13 +01:00
/**
* @internal
*/
2021-06-18 13:05:06 +02:00
protected _dependents: Set<IDependent> = new Set()
2022-01-19 13:06:13 +01:00
/**
* @internal
*/
2021-06-18 13:05:06 +02:00
protected _dependencies: Set<IDerivation<$IntentionalAny>> = new Set()
2022-01-19 13:06:13 +01:00
/**
* @internal
*/
2021-06-18 13:05:06 +02:00
protected abstract _recalculate(): V
2022-01-19 13:06:13 +01:00
/**
* @internal
*/
2021-06-18 13:05:06 +02:00
protected abstract _reactToDependencyBecomingStale(
which: IDerivation<unknown>,
): void
constructor() {}
2022-01-19 13:06:13 +01:00
/**
* Whether the derivation is hot.
*/
2021-06-18 13:05:06 +02:00
get isHot(): boolean {
return this._isHot
}
2022-01-19 13:06:13 +01:00
/**
* @internal
*/
2021-06-18 13:05:06 +02:00
protected _addDependency(d: IDerivation<$IntentionalAny>) {
if (this._dependencies.has(d)) return
this._dependencies.add(d)
if (this._isHot) d.addDependent(this._internal_markAsStale)
}
2022-01-19 13:06:13 +01:00
/**
* @internal
*/
2021-06-18 13:05:06 +02:00
protected _removeDependency(d: IDerivation<$IntentionalAny>) {
if (!this._dependencies.has(d)) return
this._dependencies.delete(d)
if (this._isHot) d.removeDependent(this._internal_markAsStale)
}
2022-01-19 13:06:13 +01:00
/**
* Returns a `Tappable` of the changes of this derivation.
*/
2021-06-18 13:05:06 +02:00
changes(ticker: Ticker): Tappable<V> {
return new DerivationEmitter(this, ticker).tappable()
}
2022-01-19 13:06:13 +01:00
/**
* 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.
*/
2021-06-18 13:05:06 +02:00
changesWithoutValues(): Tappable<void> {
return new DerivationValuelessEmitter(this).tappable()
}
2022-01-19 13:06:13 +01:00
/**
* Keep the derivation hot, even if there are no tappers (subscribers).
*/
2021-06-18 13:05:06 +02:00
keepHot() {
return this.changesWithoutValues().tap(() => {})
}
2022-01-19 13:06:13 +01:00
/**
* Convenience method that taps (subscribes to) the derivation using `this.changes(ticker).tap(fn)` and immediately calls
* the callback with the current value.
*
2022-02-23 22:53:39 +01:00
* @param ticker - The ticker to use for batching.
* @param fn - The callback to call on update.
2022-01-19 13:06:13 +01:00
*
* @see changes
*/
2021-06-18 13:05:06 +02:00
tapImmediate(ticker: Ticker, fn: (cb: V) => void): VoidFn {
const untap = this.changes(ticker).tap(fn)
fn(this.getValue())
return untap
}
2022-01-19 13:06:13 +01:00
/**
* Add a derivation as a dependent of this derivation.
*
2022-02-23 22:53:39 +01:00
* @param d - The derivation to be made a dependent of this derivation.
2022-01-19 13:06:13 +01:00
*
* @see removeDependent
*/
// TODO: document this better, what are dependents?
2021-06-18 13:05:06 +02:00
addDependent(d: IDependent) {
const hadDepsBefore = this._dependents.size > 0
this._dependents.add(d)
const hasDepsNow = this._dependents.size > 0
if (hadDepsBefore !== hasDepsNow) {
this._reactToNumberOfDependentsChange()
}
}
/**
2022-01-19 13:06:13 +01:00
* Remove a derivation as a dependent of this derivation.
*
2022-02-23 22:53:39 +01:00
* @param d - The derivation to be removed from as a dependent of this derivation.
2022-01-19 13:06:13 +01:00
*
* @see addDependent
2021-06-18 13:05:06 +02:00
*/
removeDependent(d: IDependent) {
const hadDepsBefore = this._dependents.size > 0
this._dependents.delete(d)
const hasDepsNow = this._dependents.size > 0
if (hadDepsBefore !== hasDepsNow) {
this._reactToNumberOfDependentsChange()
}
}
/**
* This is meant to be called by subclasses
*
* @sealed
2022-01-19 13:06:13 +01:00
* @internal
2021-06-18 13:05:06 +02:00
*/
protected _markAsStale(which: IDerivation<$IntentionalAny>) {
this._internal_markAsStale(which)
}
private _internal_markAsStale = (which: IDerivation<$IntentionalAny>) => {
this._reactToDependencyBecomingStale(which)
if (this._didMarkDependentsAsStale) return
this._didMarkDependentsAsStale = true
this._isFresh = false
for (const dependent of this._dependents) {
2021-06-18 13:05:06 +02:00
dependent(this)
}
2021-06-18 13:05:06 +02:00
}
2022-01-19 13:06:13 +01:00
/**
* Gets the current value of the derivation. If the value is stale, it causes the derivation to freshen.
*/
2021-06-18 13:05:06 +02:00
getValue(): V {
/**
* TODO We should prevent (or warn about) a common mistake users make, which is reading the value of
* a derivation in the body of a react component (e.g. `der.getValue()` (often via `val()`) instead of `useVal()`
* or `uesPrism()`).
*
* Although that's the most common example of this mistake, you can also find it outside of react components.
* Basically the user runs `der.getValue()` assuming the read is detected by a wrapping prism when it's not.
*
* Sometiems the derivation isn't even hot when the user assumes it is.
*
* We can fix this type of mistake by:
* 1. Warning the user when they call `getValue()` on a cold derivation.
* 2. Warning the user about calling `getValue()` on a hot-but-stale derivation
* if `getValue()` isn't called by a known mechanism like a `DerivationEmitter`.
*
* Design constraints:
* - This fix should not have a perf-penalty in production. Perhaps use a global flag + `process.env.NODE_ENV !== 'production'`
* to enable it.
* - In the case of `DerivationValuelessEmitter`, we don't control when the user calls
* `getValue()` (as opposed to `DerivationEmitter` which calls `getValue()` directly).
* Perhaps we can disable the check in that case.
* - Probably the best place to add this check is right here in this method plus some changes to `reportResulutionStart()`,
* which would have to be changed to let the caller know if there is an actual collector (a prism)
* present in its stack.
*/
2021-06-18 13:05:06 +02:00
reportResolutionStart(this)
if (!this._isFresh) {
const newValue = this._recalculate()
this._lastValue = newValue
2021-06-27 13:38:18 +02:00
if (this._isHot) {
2021-06-18 13:05:06 +02:00
this._isFresh = true
this._didMarkDependentsAsStale = false
}
}
reportResolutionEnd(this)
return this._lastValue!
}
private _reactToNumberOfDependentsChange() {
const shouldBecomeHot = this._dependents.size > 0
if (shouldBecomeHot === this._isHot) return
this._isHot = shouldBecomeHot
this._didMarkDependentsAsStale = false
this._isFresh = false
if (shouldBecomeHot) {
for (const d of this._dependencies) {
2021-06-18 13:05:06 +02:00
d.addDependent(this._internal_markAsStale)
}
2021-06-18 13:05:06 +02:00
this._keepHot()
} else {
for (const d of this._dependencies) {
2021-06-18 13:05:06 +02:00
d.removeDependent(this._internal_markAsStale)
}
2021-06-18 13:05:06 +02:00
this._becomeCold()
}
}
2022-01-19 13:06:13 +01:00
/**
* @internal
*/
2021-06-18 13:05:06 +02:00
protected _keepHot() {}
2022-01-19 13:06:13 +01:00
/**
* @internal
*/
2021-06-18 13:05:06 +02:00
protected _becomeCold() {}
2022-01-19 13:06:13 +01:00
/**
* Creates a new derivation from this derivation using the provided mapping function. The new derivation's value will be
* `fn(thisDerivation.getValue())`.
*
2022-02-23 22:53:39 +01:00
* @param fn - The mapping function to use. Note: it accepts a plain value, not a derivation.
2022-01-19 13:06:13 +01:00
*/
2021-06-18 13:05:06 +02:00
map<T>(fn: (v: V) => T): IDerivation<T> {
return map(this, fn)
}
2022-01-19 13:06:13 +01:00
/**
* 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()
* ```
*
2022-02-23 22:53:39 +01:00
* @param fn - The mapping function to use. Note: it accepts a plain value, not a derivation.
2022-01-19 13:06:13 +01:00
*/
2021-06-18 13:05:06 +02:00
flatMap<R>(
fn: (v: V) => R,
): IDerivation<R extends IDerivation<infer T> ? T : R> {
return flatMap(this, fn)
}
}