Initial OSS commit
This commit is contained in:
commit
4a7303f40a
391 changed files with 245738 additions and 0 deletions
1
packages/dataverse2/.babelrc.js
Normal file
1
packages/dataverse2/.babelrc.js
Normal file
|
@ -0,0 +1 @@
|
|||
module.exports = {}
|
1
packages/dataverse2/.gitignore
vendored
Normal file
1
packages/dataverse2/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/dist
|
203
packages/dataverse2/LICENSE
Normal file
203
packages/dataverse2/LICENSE
Normal 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.
|
||||
|
27
packages/dataverse2/package.json
Normal file
27
packages/dataverse2/package.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"name": "@theatre/dataverse2",
|
||||
"version": "1.0.0-dev",
|
||||
"license": "Apache-2.0",
|
||||
"author": {
|
||||
"name": "Aria Minaei",
|
||||
"email": "aria@theatrejs.com",
|
||||
"url": "https://github.com/AriaMinaei"
|
||||
},
|
||||
"source": "src/index.ts",
|
||||
"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.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash-es": "^4.17.21"
|
||||
}
|
||||
}
|
216
packages/dataverse2/src/Atom.ts
Normal file
216
packages/dataverse2/src/Atom.ts
Normal 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
|
||||
}
|
33
packages/dataverse2/src/Box.ts
Normal file
33
packages/dataverse2/src/Box.ts
Normal 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
|
||||
}
|
||||
}
|
84
packages/dataverse2/src/Ticker.ts
Normal file
84
packages/dataverse2/src/Ticker.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
24
packages/dataverse2/src/atom.typeTest.ts
Normal file
24
packages/dataverse2/src/atom.typeTest.ts
Normal 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))
|
||||
}
|
199
packages/dataverse2/src/derivations/AbstractDerivation.ts
Normal file
199
packages/dataverse2/src/derivations/AbstractDerivation.ts
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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 */
|
17
packages/dataverse2/src/derivations/ConstantDerivation.ts
Normal file
17
packages/dataverse2/src/derivations/ConstantDerivation.ts
Normal 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() {}
|
||||
}
|
52
packages/dataverse2/src/derivations/DerivationEmitter.ts
Normal file
52
packages/dataverse2/src/derivations/DerivationEmitter.ts
Normal 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)
|
||||
}
|
||||
}
|
53
packages/dataverse2/src/derivations/DerivationFromSource.ts
Normal file
53
packages/dataverse2/src/derivations/DerivationFromSource.ts
Normal 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() {}
|
||||
}
|
5
packages/dataverse2/src/derivations/Freshener.ts
Normal file
5
packages/dataverse2/src/derivations/Freshener.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import type {GraphNode} from './IDerivation'
|
||||
|
||||
export default class Freshener {
|
||||
schedulePeak(d: GraphNode) {}
|
||||
}
|
29
packages/dataverse2/src/derivations/IDerivation.ts
Normal file
29
packages/dataverse2/src/derivations/IDerivation.ts
Normal 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
|
||||
}
|
127
packages/dataverse2/src/derivations/flatMap.ts
Normal file
127
packages/dataverse2/src/derivations/flatMap.ts
Normal 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
|
||||
}
|
32
packages/dataverse2/src/derivations/iterateAndCountTicks.ts
Normal file
32
packages/dataverse2/src/derivations/iterateAndCountTicks.ts
Normal 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()
|
||||
}
|
||||
}
|
19
packages/dataverse2/src/derivations/iterateOver.test.ts
Normal file
19
packages/dataverse2/src/derivations/iterateOver.test.ts
Normal 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()
|
||||
})
|
||||
})
|
32
packages/dataverse2/src/derivations/iterateOver.ts
Normal file
32
packages/dataverse2/src/derivations/iterateOver.ts
Normal 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()
|
||||
}
|
||||
}
|
32
packages/dataverse2/src/derivations/map.ts
Normal file
32
packages/dataverse2/src/derivations/map.ts
Normal 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)
|
||||
}
|
|
@ -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
|
||||
}
|
225
packages/dataverse2/src/derivations/prism/prism.test.ts
Normal file
225
packages/dataverse2/src/derivations/prism/prism.test.ts
Normal 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'})
|
||||
})
|
||||
})
|
||||
})
|
331
packages/dataverse2/src/derivations/prism/prism.ts
Normal file
331
packages/dataverse2/src/derivations/prism/prism.ts
Normal 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
|
14
packages/dataverse2/src/index.ts
Normal file
14
packages/dataverse2/src/index.ts
Normal 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'
|
32
packages/dataverse2/src/integration.test.ts
Normal file
32
packages/dataverse2/src/integration.test.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import Atom, {val} from './Atom'
|
||||
import prism from './derivations/prism/prism'
|
||||
import Ticker from './Ticker'
|
||||
|
||||
describe.skip(`dataverse2 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)
|
||||
})
|
||||
})
|
||||
})
|
98
packages/dataverse2/src/pointer.ts
Normal file
98
packages/dataverse2/src/pointer.ts
Normal 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
|
1
packages/dataverse2/src/setupTestEnv.ts
Normal file
1
packages/dataverse2/src/setupTestEnv.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export {}
|
6
packages/dataverse2/src/types.ts
Normal file
6
packages/dataverse2/src/types.ts
Normal 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
|
19
packages/dataverse2/src/utils/Emitter.test.ts
Normal file
19
packages/dataverse2/src/utils/Emitter.test.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import Emitter from './Emitter'
|
||||
|
||||
describe.skip('DataVerse2.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'])
|
||||
})
|
||||
})
|
55
packages/dataverse2/src/utils/Emitter.ts
Normal file
55
packages/dataverse2/src/utils/Emitter.ts
Normal 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
|
||||
}
|
||||
}
|
56
packages/dataverse2/src/utils/EventEmitter.ts
Normal file
56
packages/dataverse2/src/utils/EventEmitter.ts
Normal 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
|
||||
}
|
||||
}
|
133
packages/dataverse2/src/utils/PathBasedReducer.ts
Normal file
133
packages/dataverse2/src/utils/PathBasedReducer.ts
Normal 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
|
||||
}
|
33
packages/dataverse2/src/utils/Stack.ts
Normal file
33
packages/dataverse2/src/utils/Stack.ts
Normal 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
|
||||
}
|
||||
}
|
91
packages/dataverse2/src/utils/Tappable.ts
Normal file
91
packages/dataverse2/src/utils/Tappable.ts
Normal 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))
|
||||
// })
|
||||
// },
|
||||
// })
|
||||
// }
|
||||
}
|
12
packages/dataverse2/src/utils/typeTestUtils.ts
Normal file
12
packages/dataverse2/src/utils/typeTestUtils.ts
Normal 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
|
42
packages/dataverse2/src/utils/updateDeep.ts
Normal file
42
packages/dataverse2/src/utils/updateDeep.ts
Normal 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)}
|
||||
}
|
||||
}
|
11
packages/dataverse2/tsconfig.json
Normal file
11
packages/dataverse2/tsconfig.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": ".temp/declarations",
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"rootDir": ".",
|
||||
"types": ["jest", "node"],
|
||||
"composite": true
|
||||
},
|
||||
"include": ["./src/**/*"]
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue