2021-06-18 13:05:06 +02:00
|
|
|
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>()
|
|
|
|
|
2022-01-19 13:06:13 +01:00
|
|
|
/**
|
|
|
|
* A wrapper type for the type a `Pointer` points to.
|
|
|
|
*/
|
2021-06-18 13:05:06 +02:00
|
|
|
export type PointerType<O> = {
|
|
|
|
$$__pointer_type: O
|
|
|
|
}
|
|
|
|
|
2022-01-19 13:06:13 +01:00
|
|
|
/**
|
|
|
|
* The type of {@link Atom} pointers. See {@link pointer|pointer()} for an
|
|
|
|
* explanation of pointers.
|
|
|
|
*
|
|
|
|
* @see Atom
|
2022-04-09 15:02:39 +02:00
|
|
|
*
|
|
|
|
* @remarks
|
|
|
|
* The Pointer type is quite tricky because it doesn't play well with `any` and other inexact types.
|
|
|
|
* Here is an example that one would expect to work, but currently doesn't:
|
|
|
|
* ```ts
|
|
|
|
* declare function expectAnyPointer(pointer: Pointer<any>): void
|
|
|
|
*
|
2022-04-09 17:09:51 +02:00
|
|
|
* expectAnyPointer(null as Pointer<{}>) // this shows as a type error because Pointer<{}> is not assignable to Pointer<any>, even though it should
|
2022-04-09 15:02:39 +02:00
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* The current solution is to just avoid using `any` with pointer-related code (or type-test it well).
|
|
|
|
* But if you enjoy solving typescript puzzles, consider fixing this :)
|
|
|
|
*
|
2022-01-19 13:06:13 +01:00
|
|
|
*/
|
2021-06-18 13:05:06 +02:00
|
|
|
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]>
|
|
|
|
} /*&
|
|
|
|
{[K in string | number]: Pointer<K extends keyof O ? O[K] : undefined>}*/
|
|
|
|
: 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
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2022-01-19 13:06:13 +01:00
|
|
|
/**
|
|
|
|
* Returns the metadata associated with the pointer. Usually the root object and
|
|
|
|
* the path.
|
|
|
|
*
|
2022-02-23 22:53:39 +01:00
|
|
|
* @param p - The pointer.
|
2022-01-19 13:06:13 +01:00
|
|
|
*/
|
2021-06-18 13:05:06 +02:00
|
|
|
export const getPointerMeta = (
|
|
|
|
p: Pointer<$IntentionalAny> | Pointer<{}> | Pointer<unknown>,
|
|
|
|
): PointerMeta => {
|
|
|
|
// @ts-ignore @todo
|
|
|
|
const meta: PointerMeta = p[
|
|
|
|
pointerMetaSymbol as unknown as $IntentionalAny
|
|
|
|
] as $IntentionalAny
|
|
|
|
return meta
|
|
|
|
}
|
|
|
|
|
2022-01-19 13:06:13 +01:00
|
|
|
/**
|
|
|
|
* Returns the root object and the path of the pointer.
|
|
|
|
*
|
|
|
|
* @example
|
|
|
|
* ```ts
|
|
|
|
* const {root, path} = getPointerParts(pointer)
|
|
|
|
* ```
|
|
|
|
*
|
2022-02-23 22:53:39 +01:00
|
|
|
* @param p - The pointer.
|
2022-01-19 13:06:13 +01:00
|
|
|
*
|
|
|
|
* @returns An object with two properties: `root`-the root object or the pointer, and `path`-the path of the pointer. `path` is an array of the property-chain.
|
|
|
|
*/
|
2021-06-18 13:05:06 +02:00
|
|
|
export const getPointerParts = (
|
|
|
|
p: Pointer<$IntentionalAny> | Pointer<{}> | Pointer<unknown>,
|
|
|
|
): {root: {}; path: PathToProp} => {
|
|
|
|
const {root, path} = getPointerMeta(p)
|
|
|
|
return {root, path}
|
|
|
|
}
|
|
|
|
|
2022-01-19 13:06:13 +01:00
|
|
|
/**
|
|
|
|
* Creates a pointer to a (nested) property of an {@link Atom}.
|
|
|
|
*
|
|
|
|
* @remarks
|
|
|
|
* Pointers are used to make derivations of properties or nested properties of
|
|
|
|
* {@link Atom|Atoms}.
|
|
|
|
*
|
|
|
|
* Pointers also allow easy construction of new pointers pointing to nested members
|
|
|
|
* of the root object, by simply using property chaining. E.g. `somePointer.a.b` will
|
|
|
|
* create a new pointer that has `'a'` and `'b'` added to the path of `somePointer`.
|
|
|
|
*
|
|
|
|
* @example
|
|
|
|
* ```ts
|
|
|
|
* // Here, sum is a derivation that updates whenever the a or b prop of someAtom does.
|
|
|
|
* const sum = prism(() => {
|
|
|
|
* return val(pointer({root: someAtom, path: ['a']})) + val(pointer({root: someAtom, path: ['b']}));
|
|
|
|
* });
|
|
|
|
*
|
|
|
|
* // Note, atoms have a convenience Atom.pointer property that points to the root,
|
|
|
|
* // which you would normally use in this situation.
|
|
|
|
* const sum = prism(() => {
|
|
|
|
* return val(someAtom.pointer.a) + val(someAtom.pointer.b);
|
|
|
|
* });
|
|
|
|
* ```
|
|
|
|
*
|
2022-02-23 22:53:39 +01:00
|
|
|
* @param args - The pointer parameters.
|
2022-01-19 13:06:13 +01:00
|
|
|
*
|
2022-02-23 22:53:39 +01:00
|
|
|
* @typeParam O - The type of the value being pointed to.
|
2022-01-19 13:06:13 +01:00
|
|
|
*/
|
|
|
|
function pointer<O>(args: {root: {}; path?: Array<string | number>}) {
|
2021-06-18 13:05:06 +02:00
|
|
|
const meta: PointerMeta = {
|
|
|
|
root: args.root as $IntentionalAny,
|
|
|
|
path: args.path ?? [],
|
|
|
|
}
|
|
|
|
const hiddenObj = {}
|
|
|
|
pointerMetaWeakMap.set(hiddenObj, meta)
|
2022-01-19 13:06:13 +01:00
|
|
|
return new Proxy(hiddenObj, handler) as Pointer<O>
|
2021-06-18 13:05:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
export default pointer
|
2021-06-27 13:37:10 +02:00
|
|
|
|
2022-01-19 13:06:13 +01:00
|
|
|
/**
|
|
|
|
* Returns whether `p` is a pointer.
|
|
|
|
*/
|
2021-06-27 13:37:10 +02:00
|
|
|
export const isPointer = (p: $IntentionalAny): p is Pointer<unknown> => {
|
|
|
|
return p && !!getPointerMeta(p)
|
|
|
|
}
|