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 @@
module.exports = {}

View file

@ -0,0 +1 @@
/dist

View file

@ -0,0 +1,203 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -0,0 +1,26 @@
{
"name": "@theatre/dataverse-experiments",
"version": "1.0.0-dev",
"license": "Apache-2.0",
"author": {
"name": "Aria Minaei",
"email": "aria@theatrejs.com",
"url": "https://github.com/AriaMinaei"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"typecheck": "yarn run _declarations:emit",
"_declarations:emit": "tsc --build ./tsconfig.json"
},
"devDependencies": {
"@types/jest": "^26.0.23",
"@types/lodash-es": "^4.17.4",
"@types/node": "^15.6.2",
"@types/react": "^17.0.9",
"typescript": "^4.4.2"
},
"dependencies": {
"lodash-es": "^4.17.21"
}
}

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)}
}
}

View file

@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": ".temp/declarations",
"lib": ["ESNext", "DOM"],
"rootDir": ".",
"types": ["jest", "node"],
"composite": true
},
"include": ["./src/**/*"]
}