Renamed dataverse2 to dataverse-experiments

This commit is contained in:
Aria Minaei 2021-10-04 20:06:12 +02:00
parent cf9b35bb4d
commit 69acb61f84
39 changed files with 8 additions and 6 deletions

View file

@ -0,0 +1,216 @@
import get from 'lodash-es/get'
import isPlainObject from 'lodash-es/isPlainObject'
import last from 'lodash-es/last'
import DerivationFromSource from './derivations/DerivationFromSource'
import type {IDerivation} from './derivations/IDerivation'
import {isDerivation} from './derivations/IDerivation'
import type {Pointer, PointerType} from './pointer'
import pointer, {getPointerMeta} from './pointer'
import type {$FixMe, $IntentionalAny} from './types'
import type {PathBasedReducer} from './utils/PathBasedReducer'
import updateDeep from './utils/updateDeep'
type Listener = (newVal: unknown) => void
enum ValueTypes {
Dict,
Array,
Other,
}
const getTypeOfValue = (v: unknown): ValueTypes => {
if (Array.isArray(v)) return ValueTypes.Array
if (isPlainObject(v)) return ValueTypes.Dict
return ValueTypes.Other
}
const getKeyOfValue = (
v: unknown,
key: string | number,
vType: ValueTypes = getTypeOfValue(v),
): unknown => {
if (vType === ValueTypes.Dict && typeof key === 'string') {
return (v as $IntentionalAny)[key]
} else if (vType === ValueTypes.Array && isValidArrayIndex(key)) {
return (v as $IntentionalAny)[key]
} else {
return undefined
}
}
const isValidArrayIndex = (key: string | number): boolean => {
const inNumber = typeof key === 'number' ? key : parseInt(key, 10)
return (
!isNaN(inNumber) &&
inNumber >= 0 &&
inNumber < Infinity &&
(inNumber | 0) === inNumber
)
}
class Scope {
children: Map<string | number, Scope> = new Map()
identityChangeListeners: Set<Listener> = new Set()
constructor(
readonly _parent: undefined | Scope,
readonly _path: (string | number)[],
) {}
addIdentityChangeListener(cb: Listener) {
this.identityChangeListeners.add(cb)
}
removeIdentityChangeListener(cb: Listener) {
this.identityChangeListeners.delete(cb)
this._checkForGC()
}
removeChild(key: string | number) {
this.children.delete(key)
this._checkForGC()
}
getChild(key: string | number) {
return this.children.get(key)
}
getOrCreateChild(key: string | number) {
let child = this.children.get(key)
if (!child) {
child = child = new Scope(this, this._path.concat([key]))
this.children.set(key, child)
}
return child
}
_checkForGC() {
if (this.identityChangeListeners.size > 0) return
if (this.children.size > 0) return
if (this._parent) {
this._parent.removeChild(last(this._path) as string | number)
}
}
}
export default class Atom<State extends {}> {
private _currentState: State
private readonly _rootScope: Scope
readonly pointer: Pointer<State>
constructor(initialState: State) {
this._currentState = initialState
this._rootScope = new Scope(undefined, [])
this.pointer = pointer({root: this as $FixMe, path: []})
}
setState(newState: State) {
const oldState = this._currentState
this._currentState = newState
this._checkUpdates(this._rootScope, oldState, newState)
}
getState() {
return this._currentState
}
getIn(path: (string | number)[]): unknown {
return path.length === 0 ? this.getState() : get(this.getState(), path)
}
reduceState: PathBasedReducer<State, State> = (
path: $IntentionalAny[],
reducer: $IntentionalAny,
) => {
const newState = updateDeep(this.getState(), path, reducer)
this.setState(newState)
return newState
}
setIn(path: $FixMe[], val: $FixMe) {
return this.reduceState(path, () => val)
}
private _checkUpdates(scope: Scope, oldState: unknown, newState: unknown) {
if (oldState === newState) return
scope.identityChangeListeners.forEach((cb) => cb(newState))
if (scope.children.size === 0) return
const oldValueType = getTypeOfValue(oldState)
const newValueType = getTypeOfValue(newState)
if (oldValueType === ValueTypes.Other && oldValueType === newValueType)
return
scope.children.forEach((childScope, childKey) => {
const oldChildVal = getKeyOfValue(oldState, childKey, oldValueType)
const newChildVal = getKeyOfValue(newState, childKey, newValueType)
this._checkUpdates(childScope, oldChildVal, newChildVal)
})
}
private _getOrCreateScopeForPath(path: (string | number)[]): Scope {
let curScope = this._rootScope
for (const pathEl of path) {
curScope = curScope.getOrCreateChild(pathEl)
}
return curScope
}
onPathValueChange(path: (string | number)[], cb: (v: unknown) => void) {
const scope = this._getOrCreateScopeForPath(path)
scope.identityChangeListeners.add(cb)
const untap = () => {
scope.identityChangeListeners.delete(cb)
}
return untap
}
}
const identityDerivationWeakMap = new WeakMap<{}, IDerivation<unknown>>()
export const valueDerivation = <P extends PointerType<$IntentionalAny>>(
pointer: P,
): IDerivation<P extends PointerType<infer T> ? T : void> => {
const meta = getPointerMeta(pointer)
let derivation = identityDerivationWeakMap.get(meta)
if (!derivation) {
const root = meta.root
if (!(root instanceof Atom)) {
throw new Error(
`Cannot run valueDerivation on a pointer whose root is not an Atom`,
)
}
const {path} = meta
derivation = new DerivationFromSource<$IntentionalAny>(
(listener) => root.onPathValueChange(path, listener),
() => root.getIn(path),
)
identityDerivationWeakMap.set(meta, derivation)
}
return derivation as $IntentionalAny
}
export const val = <P>(
pointerOrDerivationOrPlainValue: P,
): P extends PointerType<infer T>
? T
: P extends IDerivation<infer T>
? T
: unknown => {
if (isPointer(pointerOrDerivationOrPlainValue)) {
return valueDerivation(
pointerOrDerivationOrPlainValue,
).getValue() as $IntentionalAny
} else if (isDerivation(pointerOrDerivationOrPlainValue)) {
return pointerOrDerivationOrPlainValue.getValue() as $IntentionalAny
} else {
return pointerOrDerivationOrPlainValue as $IntentionalAny
}
}
export const isPointer = (p: $IntentionalAny): p is Pointer<unknown> => {
return p && p.$pointerMeta ? true : false
}

View file

@ -0,0 +1,33 @@
import DerivationFromSource from './derivations/DerivationFromSource'
import type {IDerivation} from './derivations/IDerivation'
import Emitter from './utils/Emitter'
export interface IBox<V> {
set(v: V): void
get(): V
derivation: IDerivation<V>
}
export default class Box<V> implements IBox<V> {
private _publicDerivation: IDerivation<V>
private _emitter = new Emitter<V>()
constructor(protected _value: V) {
this._publicDerivation = new DerivationFromSource(
(listener) => this._emitter.tappable.tap(listener),
this.get.bind(this),
)
}
set(v: V) {
this._value = v
this._emitter.emit(v)
}
get() {
return this._value
}
get derivation() {
return this._publicDerivation
}
}

View file

@ -0,0 +1,84 @@
type ICallback = (t: number) => void
export default class Ticker {
private _scheduledForThisOrNextTick: Set<ICallback>
private _scheduledForNextTick: Set<ICallback>
private _timeAtCurrentTick: number
private _ticking: boolean = false
constructor() {
this._scheduledForThisOrNextTick = new Set()
this._scheduledForNextTick = new Set()
this._timeAtCurrentTick = 0
}
/**
* Registers for fn to be called either on this tick or the next tick.
*
* If registerSideEffect() 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().
*
* Note that fn will be added to a Set(). Which means, if you call registerSideEffect(fn)
* with the same fn twice in a single tick, it'll only run once.
*/
onThisOrNextTick(fn: ICallback) {
this._scheduledForThisOrNextTick.add(fn)
}
/**
* Registers a side effect to be called on the next tick.
*
* @see Ticker:onThisOrNextTick()
*/
onNextTick(fn: ICallback) {
this._scheduledForNextTick.add(fn)
}
offThisOrNextTick(fn: ICallback) {
this._scheduledForThisOrNextTick.delete(fn)
}
offNextTick(fn: ICallback) {
this._scheduledForNextTick.delete(fn)
}
get time() {
if (this._ticking) {
return this._timeAtCurrentTick
} else return performance.now()
}
tick(t: number = performance.now()) {
this._ticking = true
this._timeAtCurrentTick = t
this._scheduledForNextTick.forEach((v) =>
this._scheduledForThisOrNextTick.add(v),
)
this._scheduledForNextTick.clear()
this._tick(0)
this._ticking = false
}
private _tick(iterationNumber: number): void {
const time = this.time
if (iterationNumber > 10) {
console.warn('_tick() recursing for 10 times')
}
if (iterationNumber > 100) {
throw new Error(`Maximum recursion limit for _tick()`)
}
const oldSet = this._scheduledForThisOrNextTick
this._scheduledForThisOrNextTick = new Set()
oldSet.forEach((fn) => {
fn(time)
})
if (this._scheduledForThisOrNextTick.size > 0) {
return this._tick(iterationNumber + 1)
}
}
}

View file

@ -0,0 +1,24 @@
import Atom, {val} from './Atom'
import {expectType, _any} from './utils/typeTestUtils'
;() => {
const p = new Atom<{foo: string; bar: number; optional?: boolean}>(_any)
.pointer
expectType<string>(val(p.foo))
// @ts-expect-error TypeTest
expectType<number>(val(p.foo))
expectType<number>(val(p.bar))
// @ts-expect-error TypeTest
expectType<string>(val(p.bar))
// @ts-expect-error TypeTest
expectType<{}>(val(p.nonExistent))
expectType<undefined | boolean>(val(p.optional))
// @ts-expect-error TypeTest
expectType<boolean>(val(p.optional))
// @ts-expect-error TypeTest
expectType<undefined>(val(p.optional))
// @ts-expect-error TypeTest
expectType<undefined | string>(val(p.optional))
}

View file

@ -0,0 +1,199 @@
import type {$IntentionalAny} from '../types'
import type Tappable from '../utils/Tappable'
import DerivationEmitter from './DerivationEmitter'
import flatMap from './flatMap'
import type {GraphNode, IDerivation} from './IDerivation'
import map from './map'
import {
reportResolutionEnd,
reportResolutionStart,
} from './prism/discoveryMechanism'
export default abstract class AbstractDerivation<V> implements IDerivation<V> {
readonly isDerivation: true = true
private _didMarkDependentsAsStale: boolean = false
private _isHot: boolean = false
private _isFresh: boolean = false
protected _lastValue: undefined | V = undefined
protected _dependents: Set<GraphNode> = new Set()
protected _dependencies: Set<IDerivation<$IntentionalAny>> = new Set()
/**
* _height is the maximum height of all dependents, plus one.
*
* -1 means it's not yet calculated
* 0 is reserved only for listeners
*/
private _height: number = -1
private _graphNode: GraphNode
protected abstract _recalculate(): V
protected abstract _reactToDependencyBecomingStale(
which: IDerivation<unknown>,
): void
constructor() {
const self = this
this._graphNode = {
get height() {
return self._height
},
recalculate() {
// @todo
},
}
}
get isHot(): boolean {
return this._isHot
}
get height() {
return this._height
}
protected _addDependency(d: IDerivation<$IntentionalAny>) {
if (this._dependencies.has(d)) return
this._dependencies.add(d)
if (this._isHot) d.addDependent(this._graphNode)
}
protected _removeDependency(d: IDerivation<$IntentionalAny>) {
if (!this._dependencies.has(d)) return
this._dependencies.delete(d)
if (this._isHot) d.removeDependent(this._graphNode)
}
changes(): Tappable<V> {
return new DerivationEmitter(this).tappable()
}
addDependent(d: GraphNode) {
const hadDepsBefore = this._dependents.size > 0
this._dependents.add(d)
if (d.height > this._height - 1) {
this._setHeight(d.height + 1)
}
if (!hadDepsBefore) {
this._reactToNumberOfDependentsChange()
}
}
/**
* @sealed
*/
removeDependent(d: GraphNode) {
const hadDepsBefore = this._dependents.size > 0
this._dependents.delete(d)
const hasDepsNow = this._dependents.size > 0
if (hadDepsBefore !== hasDepsNow) {
this._reactToNumberOfDependentsChange()
}
}
reportDependentHeightChange(d: GraphNode) {
if (process.env.NODE_ENV === 'development') {
if (!this._dependents.has(d)) {
throw new Error(
`Got a reportDependentHeightChange from a non-dependent.`,
)
}
}
this._recalculateHeight()
}
private _recalculateHeight() {
let maxHeightOfDependents = -1
this._dependents.forEach((d) => {
maxHeightOfDependents = Math.max(maxHeightOfDependents, d.height)
})
const newHeight = maxHeightOfDependents + 1
if (this._height !== newHeight) {
this._setHeight(newHeight)
}
}
private _setHeight(h: number) {
this._height = h
this._dependencies.forEach((d) => {
d.reportDependentHeightChange(this._graphNode)
})
}
/**
* This is meant to be called by subclasses
*
* @sealed
*/
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
this._dependents.forEach((dependent) => {
dependent.recalculate()
})
}
getValue(): V {
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) {
this._dependencies.forEach((d) => {
d.addDependent(this._graphNode)
})
this._keepHot()
} else {
this._dependencies.forEach((d) => {
d.removeDependent(this._graphNode)
})
this._becomeCold()
}
}
protected _keepHot() {}
protected _becomeCold() {}
map<T>(fn: (v: V) => T): IDerivation<T> {
return map(this, fn)
}
flatMap<R>(
fn: (v: V) => R,
): IDerivation<R extends IDerivation<infer T> ? T : R> {
return flatMap(this, fn)
}
}

View file

@ -0,0 +1,36 @@
import type {$IntentionalAny} from '../types'
import type {IDerivation} from './IDerivation'
const _any: $IntentionalAny = null
// map
;() => {
const a: IDerivation<string> = _any
// $ExpectType IDerivation<number>
// 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<string> = _any
// okay
a.flatMap((s: string) => {})
// @ts-expect-error TypeTest
a.flatMap((s: number) => {})
// $ExpectType IDerivation<number>
a.flatMap((s): IDerivation<number> => _any)
// $ExpectType IDerivation<number>
a.flatMap((s): number => _any)
}
/* eslint-enable unused-imports/no-unused-vars-ts */

View file

@ -0,0 +1,17 @@
import AbstractDerivation from './AbstractDerivation'
export default class ConstantDerivation<V> extends AbstractDerivation<V> {
_v: V
constructor(v: V) {
super()
this._v = v
return this
}
_recalculate() {
return this._v
}
_reactToDependencyBecomingStale() {}
}

View file

@ -0,0 +1,52 @@
import Emitter from '../utils/Emitter'
import type {default as Tappable} from '../utils/Tappable'
import type {GraphNode, IDerivation} from './IDerivation'
export default class DerivationEmitter<V> {
private _emitter: Emitter<V>
private _lastValue: undefined | V
private _lastValueRecorded: boolean
private _hadTappers: boolean
private _graphNode: GraphNode
constructor(private readonly _derivation: IDerivation<V>) {
this._emitter = new Emitter()
this._graphNode = {
height: 0,
recalculate: () => {
this._emit()
},
}
this._emitter.onNumberOfTappersChange(() => {
this._reactToNumberOfTappersChange()
})
this._hadTappers = false
this._lastValueRecorded = false
this._lastValue = undefined
return this
}
private _reactToNumberOfTappersChange() {
const hasTappers = this._emitter.hasTappers()
if (hasTappers !== this._hadTappers) {
this._hadTappers = hasTappers
if (hasTappers) {
this._derivation.addDependent(this._graphNode)
} else {
this._derivation.removeDependent(this._graphNode)
}
}
}
tappable(): Tappable<V> {
return this._emitter.tappable
}
private _emit = () => {
const newValue = this._derivation.getValue()
if (newValue === this._lastValue && this._lastValueRecorded === true) return
this._lastValue = newValue
this._lastValueRecorded = true
this._emitter.emit(newValue)
}
}

View file

@ -0,0 +1,53 @@
import type {VoidFn} from '../types'
import AbstractDerivation from './AbstractDerivation'
const noop = () => {}
export default class DerivationFromSource<V> extends AbstractDerivation<V> {
private _untapFromChanges: () => void
private _cachedValue: undefined | V
private _hasCachedValue: boolean
constructor(
private readonly _tapToSource: (listener: (newValue: V) => void) => VoidFn,
private readonly _getValueFromSource: () => V,
) {
super()
this._untapFromChanges = noop
this._cachedValue = undefined
this._hasCachedValue = false
}
_recalculate() {
if (this.isHot) {
if (!this._hasCachedValue) {
this._cachedValue = this._getValueFromSource()
this._hasCachedValue = true
}
return this._cachedValue as V
} else {
return this._getValueFromSource()
}
}
_keepHot() {
this._hasCachedValue = false
this._cachedValue = undefined
this._untapFromChanges = this._tapToSource((newValue) => {
this._hasCachedValue = true
this._cachedValue = newValue
this._markAsStale(this)
})
}
_becomeCold() {
this._untapFromChanges()
this._untapFromChanges = noop
this._hasCachedValue = false
this._cachedValue = undefined
}
_reactToDependencyBecomingStale() {}
}

View file

@ -0,0 +1,5 @@
import type {GraphNode} from './IDerivation'
export default class Freshener {
schedulePeak(d: GraphNode) {}
}

View file

@ -0,0 +1,29 @@
import type Tappable from '../utils/Tappable'
export type GraphNode = {
height: number
recalculate(): void
}
export interface IDerivation<V> {
isDerivation: true
isHot: boolean
changes(): Tappable<V>
addDependent(d: GraphNode): void
removeDependent(d: GraphNode): void
reportDependentHeightChange(d: GraphNode): void
getValue(): V
map<T>(fn: (v: V) => T): IDerivation<T>
flatMap<R>(
fn: (v: V) => R,
): IDerivation<R extends IDerivation<infer T> ? T : R>
}
export function isDerivation(d: any): d is IDerivation<unknown> {
return d && d.isDerivation && d.isDerivation === true
}

View file

@ -0,0 +1,127 @@
import type {$FixMe} from '../types'
import AbstractDerivation from './AbstractDerivation'
import type {IDerivation} from './IDerivation'
enum UPDATE_NEEDED_FROM {
none = 0,
dep = 1,
inner = 2,
}
const makeFlatMapDerivationClass = () => {
class FlatMapDerivation<V, DepType> extends AbstractDerivation<V> {
private _innerDerivation: undefined | null | IDerivation<V>
private _staleDependency: UPDATE_NEEDED_FROM
static displayName = 'flatMap'
constructor(
readonly _depDerivation: IDerivation<DepType>,
readonly _fn: (v: DepType) => IDerivation<V> | V,
) {
super()
this._innerDerivation = undefined
this._staleDependency = UPDATE_NEEDED_FROM.dep
this._addDependency(_depDerivation)
return this
}
_recalculateHot() {
const updateNeededFrom = this._staleDependency
this._staleDependency = UPDATE_NEEDED_FROM.none
if (updateNeededFrom === UPDATE_NEEDED_FROM.inner) {
// @ts-ignore
return this._innerDerivation.getValue()
}
const possibleInnerDerivation = this._fn(this._depDerivation.getValue())
if (possibleInnerDerivation instanceof AbstractDerivation) {
this._innerDerivation = possibleInnerDerivation
this._addDependency(possibleInnerDerivation)
return possibleInnerDerivation.getValue()
} else {
return possibleInnerDerivation
}
}
protected _recalculateCold() {
const possibleInnerDerivation = this._fn(this._depDerivation.getValue())
if (possibleInnerDerivation instanceof AbstractDerivation) {
return possibleInnerDerivation.getValue()
} else {
return possibleInnerDerivation
}
}
protected _recalculate() {
return this.isHot ? this._recalculateHot() : this._recalculateCold()
}
protected _reactToDependencyBecomingStale(
msgComingFrom: IDerivation<unknown>,
) {
const updateNeededFrom =
msgComingFrom === this._depDerivation
? UPDATE_NEEDED_FROM.dep
: UPDATE_NEEDED_FROM.inner
if (
updateNeededFrom === UPDATE_NEEDED_FROM.inner &&
msgComingFrom !== this._innerDerivation
) {
throw Error(
`got a _pipostale() from neither the dep nor the inner derivation`,
)
}
if (this._staleDependency === UPDATE_NEEDED_FROM.none) {
this._staleDependency = updateNeededFrom
if (updateNeededFrom === UPDATE_NEEDED_FROM.dep) {
this._removeInnerDerivation()
}
} else if (this._staleDependency === UPDATE_NEEDED_FROM.dep) {
} else {
if (updateNeededFrom === UPDATE_NEEDED_FROM.dep) {
this._staleDependency = UPDATE_NEEDED_FROM.dep
this._removeInnerDerivation()
}
}
}
private _removeInnerDerivation() {
if (this._innerDerivation) {
this._removeDependency(this._innerDerivation)
this._innerDerivation = undefined
}
}
protected _keepHot() {
this._staleDependency = UPDATE_NEEDED_FROM.dep
this.getValue()
}
protected _becomeCold() {
this._staleDependency = UPDATE_NEEDED_FROM.dep
this._removeInnerDerivation()
}
}
return FlatMapDerivation
}
let cls: ReturnType<typeof makeFlatMapDerivationClass> | undefined = undefined
export default function flatMap<V, R>(
dep: IDerivation<V>,
fn: (v: V) => R,
): IDerivation<R extends IDerivation<infer T> ? T : R> {
if (!cls) {
cls = makeFlatMapDerivationClass()
}
return new cls(dep, fn) as $FixMe
}

View file

@ -0,0 +1,32 @@
import {isPointer, valueDerivation} from '../Atom'
import type {Pointer} from '../pointer'
import type {IDerivation} from './IDerivation'
import {isDerivation} from './IDerivation'
export default function* iterateAndCountTicks<V>(
pointerOrDerivation: IDerivation<V> | Pointer<V>,
): Generator<{value: V; ticks: number}, void, void> {
let d
if (isPointer(pointerOrDerivation)) {
d = valueDerivation(pointerOrDerivation) as IDerivation<V>
} else if (isDerivation(pointerOrDerivation)) {
d = pointerOrDerivation
} else {
throw new Error(`Only pointers and derivations are supported`)
}
let ticksCountedSinceLastYield = 0
const untap = d.changes().tap(() => {
ticksCountedSinceLastYield++
})
try {
while (true) {
const ticks = ticksCountedSinceLastYield
ticksCountedSinceLastYield = 0
yield {value: d.getValue(), ticks}
}
} finally {
untap()
}
}

View file

@ -0,0 +1,19 @@
import Atom from '../Atom'
import iterateOver from './iterateOver'
describe.skip(`iterateOver()`, () => {
test('it should work', () => {
const a = new Atom({a: 0})
let iter = iterateOver(a.pointer.a)
expect(iter.next().value).toEqual(0)
a.setIn(['a'], 1)
a.setIn(['a'], 2)
expect(iter.next()).toMatchObject({value: 2, done: false})
iter.return()
iter = iterateOver(a.pointer.a)
expect(iter.next().value).toEqual(2)
a.setIn(['a'], 3)
expect(iter.next()).toMatchObject({done: false, value: 3})
iter.return()
})
})

View file

@ -0,0 +1,32 @@
import {isPointer, valueDerivation} from '../Atom'
import type {Pointer} from '../pointer'
import Ticker from '../Ticker'
import type {IDerivation} from './IDerivation'
import {isDerivation} from './IDerivation'
export default function* iterateOver<V>(
pointerOrDerivation: IDerivation<V> | Pointer<V>,
): Generator<V, void, void> {
let d
if (isPointer(pointerOrDerivation)) {
d = valueDerivation(pointerOrDerivation) as IDerivation<V>
} else if (isDerivation(pointerOrDerivation)) {
d = pointerOrDerivation
} else {
throw new Error(`Only pointers and derivations are supported`)
}
const ticker = new Ticker()
const untap = d.changes().tap((v) => {})
try {
while (true) {
ticker.tick()
yield d.getValue()
}
} finally {
untap()
}
}

View file

@ -0,0 +1,32 @@
import AbstractDerivation from './AbstractDerivation'
import type {IDerivation} from './IDerivation'
// Exporting from a function because of the circular dependency with AbstractDerivation
const makeMapDerivationClass = () =>
class MapDerivation<T, V> extends AbstractDerivation<V> {
constructor(
private readonly _dep: IDerivation<T>,
private readonly _fn: (t: T) => V,
) {
super()
this._addDependency(_dep)
}
_recalculate() {
return this._fn(this._dep.getValue())
}
_reactToDependencyBecomingStale() {}
}
let cls: ReturnType<typeof makeMapDerivationClass> | undefined = undefined
export default function flatMap<V, R>(
dep: IDerivation<V>,
fn: (v: V) => R,
): IDerivation<R> {
if (!cls) {
cls = makeMapDerivationClass()
}
return new cls(dep, fn)
}

View file

@ -0,0 +1,50 @@
import type {$IntentionalAny} from '../../types'
import Stack from '../../utils/Stack'
import type {IDerivation} from '../IDerivation'
const noop = () => {}
const stack = new Stack<Collector>()
const noopCollector: Collector = noop
type Collector = (d: IDerivation<$IntentionalAny>) => void
export const collectObservedDependencies = (
cb: () => void,
collector: Collector,
) => {
stack.push(collector)
cb()
stack.pop()
}
export const startIgnoringDependencies = () => {
stack.push(noopCollector)
}
export const stopIgnoringDependencies = () => {
if (stack.peek() !== noopCollector) {
if (process.env.NODE_ENV === 'development') {
console.warn('This should never happen')
}
} else {
stack.pop()
}
}
export const reportResolutionStart = (d: IDerivation<$IntentionalAny>) => {
const possibleCollector = stack.peek()
if (possibleCollector) {
possibleCollector(d)
}
stack.push(noopCollector)
}
export const reportResolutionEnd = (_d: IDerivation<$IntentionalAny>) => {
stack.pop()
}
export const isCollectingDependencies = () => {
return stack.peek() !== noopCollector
}

View file

@ -0,0 +1,225 @@
import Atom, {val} from '../../Atom'
import Ticker from '../../Ticker'
import type {$FixMe, $IntentionalAny} from '../../types'
import ConstantDerivation from '../ConstantDerivation'
import iterateAndCountTicks from '../iterateAndCountTicks'
import prism, {PrismDerivation} from './prism'
describe.skip('prism', () => {
let ticker: Ticker
beforeEach(() => {
ticker = new Ticker()
})
it('should work', () => {
const o = new Atom({foo: 'foo'})
const d = new PrismDerivation(() => {
return val(o.pointer.foo) + 'boo'
})
expect(d.getValue()).toEqual('fooboo')
const changes: Array<$FixMe> = []
d.changes().tap((c) => {
changes.push(c)
})
o.reduceState(['foo'], () => 'foo2')
ticker.tick()
expect(changes).toMatchObject(['foo2boo'])
})
it('should only collect immediate dependencies', () => {
const aD = new ConstantDerivation(1)
const bD = aD.map((v) => v * 2)
const cD = prism(() => {
return bD.getValue()
})
expect(cD.getValue()).toEqual(2)
expect((cD as $IntentionalAny)._dependencies.size).toEqual(1)
})
describe('prism.ref()', () => {
it('should work', () => {
const theAtom: Atom<{n: number}> = new Atom({n: 2})
const isEvenD = prism((): {isEven: boolean} => {
const ref = prism.ref<{isEven: boolean} | undefined>('cache', undefined)
const currentN = val(theAtom.pointer.n)
const isEven = currentN % 2 === 0
if (ref.current && ref.current.isEven === isEven) {
return ref.current
} else {
ref.current = {isEven}
return ref.current
}
})
const iterator = iterateAndCountTicks(isEvenD)
theAtom.reduceState(['n'], () => 3)
expect(iterator.next().value).toMatchObject({
value: {isEven: false},
ticks: 0,
})
theAtom.reduceState(['n'], () => 5)
theAtom.reduceState(['n'], () => 7)
expect(iterator.next().value).toMatchObject({
value: {isEven: false},
ticks: 1,
})
theAtom.reduceState(['n'], () => 2)
theAtom.reduceState(['n'], () => 4)
expect(iterator.next().value).toMatchObject({
value: {isEven: true},
ticks: 1,
})
expect(iterator.next().value).toMatchObject({
value: {isEven: true},
ticks: 0,
})
})
})
describe('prism.effect()', () => {
it('should work', async () => {
let iteration = 0
const sequence: unknown[] = []
let deps: unknown[] = []
const a = new Atom({letter: 'a'})
const derivation = prism(() => {
const n = val(a.pointer.letter)
const iterationAtTimeOfCall = iteration
sequence.push({derivationCall: iterationAtTimeOfCall})
prism.effect(
'f',
() => {
sequence.push({effectCall: iterationAtTimeOfCall})
return () => {
sequence.push({cleanupCall: iterationAtTimeOfCall})
}
},
[...deps],
)
return n
})
const untap = derivation.changes().tap((change) => {
sequence.push({change})
})
expect(sequence).toMatchObject([{derivationCall: 0}, {effectCall: 0}])
sequence.length = 0
iteration++
a.setIn(['letter'], 'b')
ticker.tick()
expect(sequence).toMatchObject([{derivationCall: 1}, {change: 'b'}])
sequence.length = 0
deps = [1]
iteration++
a.setIn(['letter'], 'c')
ticker.tick()
expect(sequence).toMatchObject([
{derivationCall: 2},
{cleanupCall: 0},
{effectCall: 2},
{change: 'c'},
])
sequence.length = 0
untap()
// takes a tick before untap takes effect
await new Promise((resolve) => setTimeout(resolve, 1))
expect(sequence).toMatchObject([{cleanupCall: 2}])
})
})
describe('prism.memo()', () => {
it('should work', async () => {
let iteration = 0
const sequence: unknown[] = []
let deps: unknown[] = []
const a = new Atom({letter: 'a'})
const derivation = prism(() => {
const n = val(a.pointer.letter)
const iterationAtTimeOfCall = iteration
sequence.push({derivationCall: iterationAtTimeOfCall})
const resultOfMemo = prism.memo(
'memo',
() => {
sequence.push({memoCall: iterationAtTimeOfCall})
return iterationAtTimeOfCall
},
[...deps],
)
sequence.push({resultOfMemo})
return n
})
const untap = derivation.changes().tap((change) => {
sequence.push({change})
})
expect(sequence).toMatchObject([
{derivationCall: 0},
{memoCall: 0},
{resultOfMemo: 0},
])
sequence.length = 0
iteration++
a.setIn(['letter'], 'b')
ticker.tick()
expect(sequence).toMatchObject([
{derivationCall: 1},
{resultOfMemo: 0},
{change: 'b'},
])
sequence.length = 0
deps = [1]
iteration++
a.setIn(['letter'], 'c')
ticker.tick()
expect(sequence).toMatchObject([
{derivationCall: 2},
{memoCall: 2},
{resultOfMemo: 2},
{change: 'c'},
])
sequence.length = 0
untap()
})
})
describe(`prism.scope()`, () => {
it('should prevent name conflicts', () => {
const d = prism(() => {
const thisNameWillBeUsedForBothMemos = 'blah'
const a = prism.scope('a', () => {
return prism.memo(thisNameWillBeUsedForBothMemos, () => 'a', [])
})
const b = prism.scope('b', () => {
return prism.memo(thisNameWillBeUsedForBothMemos, () => 'b', [])
})
return {a, b}
})
expect(d.getValue()).toMatchObject({a: 'a', b: 'b'})
})
})
})

View file

@ -0,0 +1,331 @@
import Box from '../../Box'
import type {$IntentionalAny, VoidFn} from '../../types'
import Stack from '../../utils/Stack'
import AbstractDerivation from '../AbstractDerivation'
import type {IDerivation} from '../IDerivation'
import {
collectObservedDependencies,
startIgnoringDependencies,
stopIgnoringDependencies,
} from './discoveryMechanism'
const voidFn = () => {}
export class PrismDerivation<V> extends AbstractDerivation<V> {
protected _cacheOfDendencyValues: Map<IDerivation<unknown>, unknown> =
new Map()
protected _possiblyStaleDeps = new Set<IDerivation<unknown>>()
private _prismScope = new PrismScope()
constructor(readonly _fn: () => V) {
super()
}
_recalculate() {
let value: V
if (this._possiblyStaleDeps.size > 0) {
let anActuallyStaleDepWasFound = false
startIgnoringDependencies()
for (const dep of this._possiblyStaleDeps) {
if (this._cacheOfDendencyValues.get(dep) !== dep.getValue()) {
anActuallyStaleDepWasFound = true
break
}
}
stopIgnoringDependencies()
this._possiblyStaleDeps.clear()
if (!anActuallyStaleDepWasFound) {
// console.log('ok')
return this._lastValue!
}
}
const newDeps: Set<IDerivation<unknown>> = new Set()
this._cacheOfDendencyValues.clear()
collectObservedDependencies(
() => {
hookScopeStack.push(this._prismScope)
try {
value = this._fn()
} catch (error) {
console.error(error)
} finally {
const topOfTheStack = hookScopeStack.pop()
if (topOfTheStack !== this._prismScope) {
console.warn(
// @todo guide the user to report the bug in an issue
`The Prism hook stack has slipped. This is a bug.`,
)
}
}
},
(observedDep) => {
newDeps.add(observedDep)
this._addDependency(observedDep)
},
)
this._dependencies.forEach((dep) => {
if (!newDeps.has(dep)) {
this._removeDependency(dep)
}
})
this._dependencies = newDeps
startIgnoringDependencies()
newDeps.forEach((dep) => {
this._cacheOfDendencyValues.set(dep, dep.getValue())
})
stopIgnoringDependencies()
return value!
}
_reactToDependencyBecomingStale(msgComingFrom: IDerivation<unknown>) {
this._possiblyStaleDeps.add(msgComingFrom)
}
_keepHot() {
this._prismScope = new PrismScope()
startIgnoringDependencies()
this.getValue()
stopIgnoringDependencies()
}
_becomeCold() {
cleanupScopeStack(this._prismScope)
this._prismScope = new PrismScope()
}
}
class PrismScope {
isPrismScope = true
private _subs: Record<string, PrismScope> = {}
sub(key: string) {
if (!this._subs[key]) {
this._subs[key] = new PrismScope()
}
return this._subs[key]
}
get subs() {
return this._subs
}
}
function cleanupScopeStack(scope: PrismScope) {
for (const [_, sub] of Object.entries(scope.subs)) {
cleanupScopeStack(sub)
}
cleanupEffects(scope)
}
function cleanupEffects(scope: PrismScope) {
const effects = effectsWeakMap.get(scope)
if (effects) {
for (const k of Object.keys(effects)) {
const effect = effects[k]
safelyRun(effect.cleanup, undefined)
}
}
effectsWeakMap.delete(scope)
}
function safelyRun<T, U>(
fn: () => T,
returnValueInCaseOfError: U,
): {success: boolean; returnValue: T | U} {
let returnValue: T | U = returnValueInCaseOfError
let success = false
try {
returnValue = fn()
success = true
} catch (error) {
setTimeout(() => {
throw error
})
}
return {success, returnValue}
}
const hookScopeStack = new Stack<PrismScope>()
const refsWeakMap = new WeakMap<PrismScope, Record<string, IRef<unknown>>>()
type IRef<T> = {
current: T
}
const effectsWeakMap = new WeakMap<PrismScope, Record<string, IEffect>>()
type IEffect = {
deps: undefined | unknown[]
cleanup: VoidFn
}
const memosWeakMap = new WeakMap<PrismScope, Record<string, IMemo>>()
type IMemo = {
deps: undefined | unknown[]
cachedValue: unknown
}
function ref<T>(key: string, initialValue: T): IRef<T> {
const scope = hookScopeStack.peek()
if (!scope) {
throw new Error(`prism.ref() is called outside of a prism() call.`)
}
let refs = refsWeakMap.get(scope)
if (!refs) {
refs = {}
refsWeakMap.set(scope, refs)
}
if (refs[key]) {
return refs[key] as $IntentionalAny as IRef<T>
} else {
const ref: IRef<T> = {
current: initialValue,
}
refs[key] = ref
return ref
}
}
function effect(key: string, cb: () => () => void, deps?: unknown[]): void {
const scope = hookScopeStack.peek()
if (!scope) {
throw new Error(`prism.effect() is called outside of a prism() call.`)
}
let effects = effectsWeakMap.get(scope)
if (!effects) {
effects = {}
effectsWeakMap.set(scope, effects)
}
if (!effects[key]) {
effects[key] = {
cleanup: voidFn,
deps: [{}],
}
}
const effect = effects[key]
if (depsHaveChanged(effect.deps, deps)) {
effect.cleanup()
startIgnoringDependencies()
effect.cleanup = safelyRun(cb, voidFn).returnValue
stopIgnoringDependencies()
effect.deps = deps
}
}
function depsHaveChanged(
oldDeps: undefined | unknown[],
newDeps: undefined | unknown[],
): boolean {
if (oldDeps === undefined || newDeps === undefined) {
return true
} else if (oldDeps.length !== newDeps.length) {
return true
} else {
return oldDeps.some((el, i) => el !== newDeps[i])
}
}
function memo<T>(
key: string,
fn: () => T,
deps: undefined | $IntentionalAny[],
): T {
const scope = hookScopeStack.peek()
if (!scope) {
throw new Error(`prism.memo() is called outside of a prism() call.`)
}
let memos = memosWeakMap.get(scope)
if (!memos) {
memos = {}
memosWeakMap.set(scope, memos)
}
if (!memos[key]) {
memos[key] = {
cachedValue: null,
deps: [{}],
}
}
const memo = memos[key]
if (depsHaveChanged(memo.deps, deps)) {
startIgnoringDependencies()
memo.cachedValue = safelyRun(fn, undefined).returnValue
stopIgnoringDependencies()
memo.deps = deps
}
return memo.cachedValue as $IntentionalAny as T
}
function state<T>(key: string, initialValue: T): [T, (val: T) => void] {
const {b, setValue} = prism.memo(
'state/' + key,
() => {
const b = new Box<T>(initialValue)
const setValue = (val: T) => b.set(val)
return {b, setValue}
},
[],
)
return [b.derivation.getValue(), setValue]
}
function ensurePrism(): void {
const scope = hookScopeStack.peek()
if (!scope) {
throw new Error(`The parent function is called outside of a prism() call.`)
}
}
function scope<T>(key: string, fn: () => T): T {
const parentScope = hookScopeStack.peek()
if (!parentScope) {
throw new Error(`prism.memo() is called outside of a prism() call.`)
}
const subScope = parentScope.sub(key)
hookScopeStack.push(subScope)
const ret = safelyRun(fn, undefined).returnValue
hookScopeStack.pop()
return ret as $IntentionalAny as T
}
type IPrismFn = {
<T>(fn: () => T): IDerivation<T>
ref: typeof ref
effect: typeof effect
memo: typeof memo
ensurePrism: typeof ensurePrism
state: typeof state
scope: typeof scope
}
const prism: IPrismFn = (fn) => {
return new PrismDerivation(fn)
}
prism.ref = ref
prism.effect = effect
prism.memo = memo
prism.ensurePrism = ensurePrism
prism.state = state
prism.scope = scope
export default prism

View file

@ -0,0 +1,14 @@
export {default as Atom, isPointer, val, valueDerivation} from './Atom'
export {default as Box} from './Box'
export type {IBox} from './Box'
export {default as AbstractDerivation} from './derivations/AbstractDerivation'
export {default as ConstantDerivation} from './derivations/ConstantDerivation'
export {default as DerivationFromSource} from './derivations/DerivationFromSource'
export {isDerivation} from './derivations/IDerivation'
export type {IDerivation} from './derivations/IDerivation'
export {default as iterateAndCountTicks} from './derivations/iterateAndCountTicks'
export {default as iterateOver} from './derivations/iterateOver'
export {default as prism} from './derivations/prism/prism'
export {default as pointer, getPointerParts} from './pointer'
export type {Pointer} from './pointer'
export {default as Ticker} from './Ticker'

View file

@ -0,0 +1,32 @@
import Atom, {val} from './Atom'
import prism from './derivations/prism/prism'
import Ticker from './Ticker'
describe.skip(`dataverse-experiments integration tests`, () => {
describe(`identity pointers`, () => {
it(`should work`, () => {
const data = {foo: 'hi', bar: 0}
const a = new Atom(data)
const dataP = a.pointer
const bar = dataP.bar
expect(val(bar)).toEqual(0)
const d = prism(() => {
return val(bar)
})
expect(d.getValue()).toEqual(0)
const ticker = new Ticker()
const changes: number[] = []
d.changes().tap((c) => {
changes.push(c)
})
a.setState({...data, bar: 1})
ticker.tick()
expect(changes).toHaveLength(1)
expect(changes[0]).toEqual(1)
a.setState({...data, bar: 1})
ticker.tick()
expect(changes).toHaveLength(1)
})
})
})

View file

@ -0,0 +1,98 @@
import type {$IntentionalAny} from './types'
type PathToProp = Array<string | number>
type PointerMeta = {
root: {}
path: (string | number)[]
}
export type UnindexableTypesForPointer =
| number
| string
| boolean
| null
| void
| undefined
| Function // eslint-disable-line @typescript-eslint/ban-types
export type UnindexablePointer = {
[K in $IntentionalAny]: Pointer<undefined>
}
const pointerMetaWeakMap = new WeakMap<{}, PointerMeta>()
export type PointerType<O> = {
$$__pointer_type: O
}
export type Pointer<O> = PointerType<O> &
(O extends UnindexableTypesForPointer
? UnindexablePointer
: unknown extends O
? UnindexablePointer
: O extends (infer T)[]
? Pointer<T>[]
: O extends {}
? {[K in keyof O]-?: Pointer<O[K]>}
: UnindexablePointer)
const pointerMetaSymbol = Symbol('pointerMeta')
const cachedSubPointersWeakMap = new WeakMap<
{},
Record<string | number, Pointer<unknown>>
>()
const handler = {
get(obj: {}, prop: string | typeof pointerMetaSymbol): $IntentionalAny {
if (prop === pointerMetaSymbol) return pointerMetaWeakMap.get(obj)!
let subs = cachedSubPointersWeakMap.get(obj)
if (!subs) {
subs = {}
cachedSubPointersWeakMap.set(obj, subs)
}
if (subs[prop]) return subs[prop]
const meta = pointerMetaWeakMap.get(obj)!
const subPointer = pointer({root: meta.root, path: [...meta.path, prop]})
subs[prop] = subPointer
return subPointer
},
}
export const getPointerMeta = (p: Pointer<$IntentionalAny>): PointerMeta => {
const meta: PointerMeta = p[
pointerMetaSymbol as unknown as $IntentionalAny
] as $IntentionalAny
return meta
}
export const getPointerParts = (
p: Pointer<$IntentionalAny>,
): {root: {}; path: PathToProp} => {
const {root, path} = getPointerMeta(p)
return {root, path}
}
function pointer<O>({
root,
path,
}: {
root: {}
path: Array<string | number>
}): Pointer<O>
function pointer(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>
}
export default pointer

View file

@ -0,0 +1 @@
export {}

View file

@ -0,0 +1,6 @@
/** For `any`s that aren't meant to stay `any`*/
export type $FixMe = any
/** For `any`s that we don't care about */
export type $IntentionalAny = any
export type VoidFn = () => void

View file

@ -0,0 +1,19 @@
import Emitter from './Emitter'
describe.skip('dataverse-experiments.Emitter', () => {
it('should work', () => {
const e: Emitter<string> = new Emitter()
e.emit('no one will see this')
e.emit('nor this')
const tappedEvents: string[] = []
const untap = e.tappable.tap((payload) => {
tappedEvents.push(payload)
})
e.emit('foo')
e.emit('bar')
untap()
e.emit('baz')
expect(tappedEvents).toMatchObject(['foo', 'bar'])
})
})

View file

@ -0,0 +1,55 @@
import Tappable from './Tappable'
type Tapper<V> = (v: V) => void
type Untap = () => void
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)
constructor() {
this._lastTapperId = 0
this._tappers = new Map()
this.tappable = new Tappable({
tapToSource: (cb: Tapper<V>) => {
return this._tap(cb)
},
})
}
_tap(cb: Tapper<V>): Untap {
const tapperId = this._lastTapperId++
this._tappers.set(tapperId, cb)
this._onNumberOfTappersChangeListener &&
this._onNumberOfTappersChangeListener(this._tappers.size)
return () => {
this._removeTapperById(tapperId)
}
}
_removeTapperById(id: number) {
const oldSize = this._tappers.size
this._tappers.delete(id)
const newSize = this._tappers.size
if (oldSize !== newSize) {
this._onNumberOfTappersChangeListener &&
this._onNumberOfTappersChangeListener(this._tappers.size)
}
}
emit(payload: V) {
this._tappers.forEach((cb) => {
cb(payload)
})
}
hasTappers() {
return this._tappers.size !== 0
}
onNumberOfTappersChange(cb: (n: number) => void) {
this._onNumberOfTappersChangeListener = cb
}
}

View file

@ -0,0 +1,56 @@
import forEach from 'lodash-es/forEach'
import without from 'lodash-es/without'
import type {$FixMe} from '../types'
type Listener = (v: $FixMe) => void
/**
* A simple barebones event emitter
*/
export default class EventEmitter {
_listenersByType: {[eventName: string]: Array<Listener>}
constructor() {
this._listenersByType = {}
}
addEventListener(eventName: string, listener: Listener) {
const listeners =
this._listenersByType[eventName] ||
(this._listenersByType[eventName] = [])
listeners.push(listener)
return this
}
removeEventListener(eventName: string, listener: Listener) {
const listeners = this._listenersByType[eventName]
if (listeners) {
const newListeners = without(listeners, listener)
if (newListeners.length === 0) {
delete this._listenersByType[eventName]
} else {
this._listenersByType[eventName] = newListeners
}
}
return this
}
emit(eventName: string, payload: unknown) {
const listeners = this.getListenersFor(eventName)
if (listeners) {
forEach(listeners, (listener) => {
listener(payload)
})
}
}
getListenersFor(eventName: string) {
return this._listenersByType[eventName]
}
hasListenersFor(eventName: string) {
return this.getListenersFor(eventName) ? true : false
}
}

View file

@ -0,0 +1,133 @@
export type PathBasedReducer<S, ReturnType> = {
<
A0 extends keyof S,
A1 extends keyof S[A0],
A2 extends keyof S[A0][A1],
A3 extends keyof S[A0][A1][A2],
A4 extends keyof S[A0][A1][A2][A3],
A5 extends keyof S[A0][A1][A2][A3][A4],
A6 extends keyof S[A0][A1][A2][A3][A4][A5],
A7 extends keyof S[A0][A1][A2][A3][A4][A5][A6],
A8 extends keyof S[A0][A1][A2][A3][A4][A5][A6][A7],
A9 extends keyof S[A0][A1][A2][A3][A4][A5][A6][A7][A8],
A10 extends keyof S[A0][A1][A2][A3][A4][A5][A6][A7][A8][A9],
>(
addr: [A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10],
reducer: (
d: S[A0][A1][A2][A3][A4][A5][A6][A7][A8][A9][A10],
) => S[A0][A1][A2][A3][A4][A5][A6][A7][A8][A9][A10],
): ReturnType
<
A0 extends keyof S,
A1 extends keyof S[A0],
A2 extends keyof S[A0][A1],
A3 extends keyof S[A0][A1][A2],
A4 extends keyof S[A0][A1][A2][A3],
A5 extends keyof S[A0][A1][A2][A3][A4],
A6 extends keyof S[A0][A1][A2][A3][A4][A5],
A7 extends keyof S[A0][A1][A2][A3][A4][A5][A6],
A8 extends keyof S[A0][A1][A2][A3][A4][A5][A6][A7],
A9 extends keyof S[A0][A1][A2][A3][A4][A5][A6][A7][A8],
>(
addr: [A0, A1, A2, A3, A4, A5, A6, A7, A8, A9],
reducer: (
d: S[A0][A1][A2][A3][A4][A5][A6][A7][A8][A9],
) => S[A0][A1][A2][A3][A4][A5][A6][A7][A8][A9],
): ReturnType
<
A0 extends keyof S,
A1 extends keyof S[A0],
A2 extends keyof S[A0][A1],
A3 extends keyof S[A0][A1][A2],
A4 extends keyof S[A0][A1][A2][A3],
A5 extends keyof S[A0][A1][A2][A3][A4],
A6 extends keyof S[A0][A1][A2][A3][A4][A5],
A7 extends keyof S[A0][A1][A2][A3][A4][A5][A6],
A8 extends keyof S[A0][A1][A2][A3][A4][A5][A6][A7],
>(
addr: [A0, A1, A2, A3, A4, A5, A6, A7, A8],
reducer: (
d: S[A0][A1][A2][A3][A4][A5][A6][A7][A8],
) => S[A0][A1][A2][A3][A4][A5][A6][A7][A8],
): ReturnType
<
A0 extends keyof S,
A1 extends keyof S[A0],
A2 extends keyof S[A0][A1],
A3 extends keyof S[A0][A1][A2],
A4 extends keyof S[A0][A1][A2][A3],
A5 extends keyof S[A0][A1][A2][A3][A4],
A6 extends keyof S[A0][A1][A2][A3][A4][A5],
A7 extends keyof S[A0][A1][A2][A3][A4][A5][A6],
>(
addr: [A0, A1, A2, A3, A4, A5, A6, A7],
reducer: (
d: S[A0][A1][A2][A3][A4][A5][A6][A7],
) => S[A0][A1][A2][A3][A4][A5][A6][A7],
): ReturnType
<
A0 extends keyof S,
A1 extends keyof S[A0],
A2 extends keyof S[A0][A1],
A3 extends keyof S[A0][A1][A2],
A4 extends keyof S[A0][A1][A2][A3],
A5 extends keyof S[A0][A1][A2][A3][A4],
A6 extends keyof S[A0][A1][A2][A3][A4][A5],
>(
addr: [A0, A1, A2, A3, A4, A5, A6],
reducer: (
d: S[A0][A1][A2][A3][A4][A5][A6],
) => S[A0][A1][A2][A3][A4][A5][A6],
): ReturnType
<
A0 extends keyof S,
A1 extends keyof S[A0],
A2 extends keyof S[A0][A1],
A3 extends keyof S[A0][A1][A2],
A4 extends keyof S[A0][A1][A2][A3],
A5 extends keyof S[A0][A1][A2][A3][A4],
>(
addr: [A0, A1, A2, A3, A4, A5],
reducer: (d: S[A0][A1][A2][A3][A4][A5]) => S[A0][A1][A2][A3][A4][A5],
): ReturnType
<
A0 extends keyof S,
A1 extends keyof S[A0],
A2 extends keyof S[A0][A1],
A3 extends keyof S[A0][A1][A2],
A4 extends keyof S[A0][A1][A2][A3],
>(
addr: [A0, A1, A2, A3, A4],
reducer: (d: S[A0][A1][A2][A3][A4]) => S[A0][A1][A2][A3][A4],
): ReturnType
<
A0 extends keyof S,
A1 extends keyof S[A0],
A2 extends keyof S[A0][A1],
A3 extends keyof S[A0][A1][A2],
>(
addr: [A0, A1, A2, A3],
reducer: (d: S[A0][A1][A2][A3]) => S[A0][A1][A2][A3],
): ReturnType
<A0 extends keyof S, A1 extends keyof S[A0], A2 extends keyof S[A0][A1]>(
addr: [A0, A1, A2],
reducer: (d: S[A0][A1][A2]) => S[A0][A1][A2],
): ReturnType
<A0 extends keyof S, A1 extends keyof S[A0]>(
addr: [A0, A1],
reducer: (d: S[A0][A1]) => S[A0][A1],
): ReturnType
<A0 extends keyof S>(addr: [A0], reducer: (d: S[A0]) => S[A0]): ReturnType
(addr: undefined[], reducer: (d: S) => S): ReturnType
}

View file

@ -0,0 +1,33 @@
interface Node<Data> {
next: undefined | Node<Data>
data: Data
}
/**
* Just a simple LinkedList
*/
export default class Stack<Data> {
_head: undefined | Node<Data>
constructor() {
this._head = undefined
}
peek() {
return this._head && this._head.data
}
pop() {
const head = this._head
if (!head) {
return undefined
}
this._head = head.next
return head.data
}
push(data: Data) {
const node = {next: this._head, data}
this._head = node
}
}

View file

@ -0,0 +1,91 @@
type Untap = () => void
type UntapFromSource = () => void
interface IProps<V> {
tapToSource: (cb: (payload: V) => void) => UntapFromSource
}
type Listener<V> = ((v: V) => void) | (() => void)
export default class Tappable<V> {
private _props: IProps<V>
private _tappers: Map<number, {bivarianceHack(v: V): void}['bivarianceHack']>
private _untapFromSource: null | UntapFromSource
private _lastTapperId: number
private _untapFromSourceTimeout: null | NodeJS.Timer = null
constructor(props: IProps<V>) {
this._lastTapperId = 0
this._untapFromSource = null
this._props = props
this._tappers = new Map()
}
private _check() {
if (this._untapFromSource) {
if (this._tappers.size === 0) {
this._scheduleToUntapFromSource()
/*
* this._untapFromSource()
* this._untapFromSource = null
*/
}
} else {
if (this._tappers.size !== 0) {
this._untapFromSource = this._props.tapToSource(this._cb)
}
}
}
private _scheduleToUntapFromSource() {
if (this._untapFromSourceTimeout !== null) return
this._untapFromSourceTimeout = setTimeout(() => {
this._untapFromSourceTimeout = null
if (this._tappers.size === 0) {
this._untapFromSource!()
this._untapFromSource = null
}
}, 0)
}
private _cb: any = (arg: any): void => {
this._tappers.forEach((cb) => {
cb(arg)
})
}
tap(cb: Listener<V>): Untap {
const tapperId = this._lastTapperId++
this._tappers.set(tapperId, cb)
this._check()
return () => {
this._removeTapperById(tapperId)
}
}
/*
* tapImmediate(cb: Listener<V>): Untap {
* const ret = this.tap(cb)
* return ret
* }
*/
private _removeTapperById(id: number) {
this._tappers.delete(id)
this._check()
}
// /**
// * @deprecated
// */
// map<T>(transform: {bivarianceHack(v: V): T}['bivarianceHack']): Tappable<T> {
// return new Tappable({
// tapToSource: (cb: (v: T) => void) => {
// return this.tap((v: $IntentionalAny) => {
// return cb(transform(v))
// })
// },
// })
// }
}

View file

@ -0,0 +1,12 @@
import type {$IntentionalAny} from '../types'
/**
* Useful in type tests, such as: const a: SomeType = _any
*/
export const _any: $IntentionalAny = null
/**
* Useful in typeTests. If you want to ensure that value v follows type V,
* just write `expectType<V>(v)`
*/
export const expectType = <T extends unknown>(v: T): T => v

View file

@ -0,0 +1,42 @@
import type {$FixMe, $IntentionalAny} from '../types'
export default function updateDeep<S>(
state: S,
path: (string | number | undefined)[],
reducer: (...args: $IntentionalAny[]) => $IntentionalAny,
): S {
if (path.length === 0) return reducer(state)
return hoop(state, path as $IntentionalAny, reducer)
}
const hoop = (
s: $FixMe,
path: (string | number)[],
reducer: $FixMe,
): $FixMe => {
if (path.length === 0) {
return reducer(s)
}
if (Array.isArray(s)) {
let [index, ...restOfPath] = path
index = parseInt(String(index), 10)
if (isNaN(index)) index = 0
const oldVal = s[index]
const newVal = hoop(oldVal, restOfPath, reducer)
if (oldVal === newVal) return s
const newS = [...s]
newS.splice(index, 1, newVal)
return newS
} else if (typeof s === 'object' && s !== null) {
const [key, ...restOfPath] = path
const oldVal = s[key]
const newVal = hoop(oldVal, restOfPath, reducer)
if (oldVal === newVal) return s
const newS = {...s, [key]: newVal}
return newS
} else {
const [key, ...restOfPath] = path
return {[key]: hoop(undefined, restOfPath, reducer)}
}
}