Initial OSS commit
This commit is contained in:
commit
4a7303f40a
391 changed files with 245738 additions and 0 deletions
|
@ -0,0 +1,94 @@
|
|||
import type {$IntentionalAny} from '../../types'
|
||||
import Stack from '../../utils/Stack'
|
||||
import type {IDerivation} from '../IDerivation'
|
||||
|
||||
function createMechanism() {
|
||||
const noop = () => {}
|
||||
|
||||
const stack = new Stack<Collector>()
|
||||
const noopCollector: Collector = noop
|
||||
|
||||
type Collector = (d: IDerivation<$IntentionalAny>) => void
|
||||
|
||||
const collectObservedDependencies = (
|
||||
cb: () => void,
|
||||
collector: Collector,
|
||||
) => {
|
||||
stack.push(collector)
|
||||
cb()
|
||||
stack.pop()
|
||||
}
|
||||
|
||||
const startIgnoringDependencies = () => {
|
||||
stack.push(noopCollector)
|
||||
}
|
||||
|
||||
const stopIgnoringDependencies = () => {
|
||||
if (stack.peek() !== noopCollector) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.warn('This should never happen')
|
||||
}
|
||||
} else {
|
||||
stack.pop()
|
||||
}
|
||||
}
|
||||
|
||||
const reportResolutionStart = (d: IDerivation<$IntentionalAny>) => {
|
||||
const possibleCollector = stack.peek()
|
||||
if (possibleCollector) {
|
||||
possibleCollector(d)
|
||||
}
|
||||
|
||||
stack.push(noopCollector)
|
||||
}
|
||||
|
||||
const reportResolutionEnd = (_d: IDerivation<$IntentionalAny>) => {
|
||||
stack.pop()
|
||||
}
|
||||
|
||||
const isCollectingDependencies = () => {
|
||||
return stack.peek() !== noopCollector
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'Dataverse_discoveryMechanism' as 'Dataverse_discoveryMechanism',
|
||||
collectObservedDependencies,
|
||||
startIgnoringDependencies,
|
||||
stopIgnoringDependencies,
|
||||
reportResolutionStart,
|
||||
reportResolutionEnd,
|
||||
isCollectingDependencies,
|
||||
}
|
||||
}
|
||||
|
||||
function getSharedMechanism(): ReturnType<typeof createMechanism> {
|
||||
const varName = '__dataverse_discoveryMechanism_sharedStack'
|
||||
if (global) {
|
||||
const existingMechanism: ReturnType<typeof createMechanism> | undefined =
|
||||
// @ts-ignore ignore
|
||||
global[varName]
|
||||
if (
|
||||
existingMechanism &&
|
||||
typeof existingMechanism === 'object' &&
|
||||
existingMechanism.type === 'Dataverse_discoveryMechanism'
|
||||
) {
|
||||
return existingMechanism
|
||||
} else {
|
||||
const mechanism = createMechanism()
|
||||
// @ts-ignore ignore
|
||||
global[varName] = mechanism
|
||||
return mechanism
|
||||
}
|
||||
} else {
|
||||
return createMechanism()
|
||||
}
|
||||
}
|
||||
|
||||
export const {
|
||||
collectObservedDependencies,
|
||||
startIgnoringDependencies,
|
||||
stopIgnoringDependencies,
|
||||
reportResolutionEnd,
|
||||
reportResolutionStart,
|
||||
isCollectingDependencies,
|
||||
} = getSharedMechanism()
|
228
packages/dataverse/src/derivations/prism/prism.test.ts
Normal file
228
packages/dataverse/src/derivations/prism/prism.test.ts
Normal file
|
@ -0,0 +1,228 @@
|
|||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
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('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(ticker).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(ticker).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(ticker).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'})
|
||||
})
|
||||
})
|
||||
})
|
347
packages/dataverse/src/derivations/prism/prism.ts
Normal file
347
packages/dataverse/src/derivations/prism/prism.ts
Normal file
|
@ -0,0 +1,347 @@
|
|||
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.scope() 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
|
||||
}
|
||||
|
||||
function sub<T>(
|
||||
key: string,
|
||||
fn: () => T,
|
||||
deps: undefined | $IntentionalAny[],
|
||||
): T {
|
||||
return memo(key, () => prism(fn), deps).getValue()
|
||||
}
|
||||
|
||||
function inPrism(): boolean {
|
||||
return !!hookScopeStack.peek()
|
||||
}
|
||||
|
||||
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
|
||||
sub: typeof sub
|
||||
inPrism: typeof inPrism
|
||||
}
|
||||
|
||||
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
|
||||
prism.sub = sub
|
||||
prism.inPrism = inPrism
|
||||
|
||||
export default prism
|
Loading…
Add table
Add a link
Reference in a new issue