diff --git a/packages/dataverse/src/derivations/AbstractDerivation.ts b/packages/dataverse/src/derivations/AbstractDerivation.ts deleted file mode 100644 index 1b0e25a..0000000 --- a/packages/dataverse/src/derivations/AbstractDerivation.ts +++ /dev/null @@ -1,286 +0,0 @@ -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 - -/** - * 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) - const hasDepsNow = this._dependents.size > 0 - if (hadDepsBefore !== hasDepsNow) { - this._reactToNumberOfDependentsChange() - } - } - - /** - * 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 - this._dependents.delete(d) - const hasDepsNow = this._dependents.size > 0 - if (hadDepsBefore !== hasDepsNow) { - this._reactToNumberOfDependentsChange() - } - } - - /** - * This is meant to be called by subclasses - * - * @sealed - * @internal - */ - 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) { - dependent(this) - } - } - - /** - * Gets the current value of the derivation. If the value is stale, it causes the derivation to freshen. - */ - 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. - */ - reportResolutionStart(this) - - if (!this._isFresh) { - const newValue = this._recalculate() - this._lastValue = newValue - if (this._isHot) { - 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) { - d.addDependent(this._internal_markAsStale) - } - this._keepHot() - } else { - for (const d of this._dependencies) { - d.removeDependent(this._internal_markAsStale) - } - this._becomeCold() - } - } - - /** - * @internal - */ - protected _keepHot() {} - - /** - * @internal - */ - protected _becomeCold() {} - - /** - * A simple mapping function similar to Array.map() - * - * @deprecated This is a remnant of the old monadic api. Now it's functionally equal to `prism(() => fn(der.getValue()))`, so use that instead. - */ - map(fn: (v: V) => T): IDerivation { - // TODO once prism and AbstractDerivation are merged into one, we should replace this with prism(() => fn(der.getValue())) - 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. - * - * @deprecated This is a remnant of the old monadic api. Now it's functionally equal to `prism(() => val(fn(val(der))))` - * - * @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> { - // TODO once prism and AbstractDerivation are merged into one, we should replace this with prism(() => val(fn(val(der)))) - return flatMap(this, fn) - } -} diff --git a/packages/dataverse/src/derivations/AbstractDerivation.typeTest.ts b/packages/dataverse/src/derivations/AbstractDerivation.typeTest.ts deleted file mode 100644 index 2aa7fcd..0000000 --- a/packages/dataverse/src/derivations/AbstractDerivation.typeTest.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type {$IntentionalAny} from '../types' -import type {IDerivation} from './IDerivation' - -const _any: $IntentionalAny = null - -// map -;() => { - const a: IDerivation = _any - - // $ExpectType IDerivation - // eslint-disable-next-line unused-imports/no-unused-vars-ts - a.map((s: string) => 10) - - // @ts-expect-error - // eslint-disable-next-line unused-imports/no-unused-vars-ts - a.map((s: number) => 10) -} - -// flatMap() -/* eslint-disable unused-imports/no-unused-vars-ts */ -;() => { - const a: IDerivation = _any - - // okay - a.flatMap((s: string) => {}) - - // @ts-expect-error TypeTest - a.flatMap((s: number) => {}) - - // $ExpectType IDerivation - a.flatMap((s): IDerivation => _any) - - // $ExpectType IDerivation - a.flatMap((s): number => _any) -} -/* eslint-enable unused-imports/no-unused-vars-ts */ diff --git a/packages/dataverse/src/index.ts b/packages/dataverse/src/index.ts index ca87c4e..5bbb2bb 100644 --- a/packages/dataverse/src/index.ts +++ b/packages/dataverse/src/index.ts @@ -8,7 +8,6 @@ export type {IdentityDerivationProvider} from './Atom' export {default as Atom, val, valueDerivation} from './Atom' export {default as Box} from './Box' export type {IBox} from './Box' -export {default as AbstractDerivation} from './derivations/AbstractDerivation' export {isDerivation} from './derivations/IDerivation' export type {IDerivation} from './derivations/IDerivation' export {default as iterateAndCountTicks} from './derivations/iterateAndCountTicks'