theatre/packages/dataverse/src/pointer.ts

190 lines
5.7 KiB
TypeScript
Raw Normal View History

2021-06-18 13:05:06 +02:00
import type {$IntentionalAny} from './types'
type PathToProp = Array<string | number>
type PointerMeta = {
root: {}
path: (string | number)[]
}
/** We are using an empty object as a WeakMap key for storing pointer meta data */
type WeakPointerKey = {}
2021-06-18 13:05:06 +02:00
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<WeakPointerKey, PointerMeta>()
const cachedSubPathPointersWeakMap = new WeakMap<
WeakPointerKey,
Map<string | number, Pointer<unknown>>
>()
2021-06-18 13:05:06 +02:00
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> = {
/**
* Only accessible via the type system.
* This is a helper for getting the underlying pointer type
* via the type space.
*/
2021-06-18 13:05:06 +02:00
$$__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
*
* @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
*
* expectAnyPointer(null as Pointer<{}>) // this shows as a type error because Pointer<{}> is not assignable to Pointer<any>, even though it should
* ```
*
* 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 :)
* Potentially, [TypeScript variance annotations in 4.7+](https://devblogs.microsoft.com/typescript/announcing-typescript-4-7-beta/#optional-variance-annotations-for-type-parameters)
* might be able to help us.
2022-01-19 13:06:13 +01:00
*/
2021-06-18 13:05:06 +02:00
export type Pointer<O> = PointerType<O> &
// `Exclude<O, undefined>` will remove `undefined` from the first type
// `undefined extends O ? undefined : never` will give us `undefined` if `O` is `... | undefined`
PointerInner<Exclude<O, undefined>, undefined extends O ? undefined : never>
// By separating the `O` (non-undefined) from the `undefined` or `never`, we
// can properly use `O extends ...` to determine the kind of potential value
// without actually discarding optionality information.
type PointerInner<O, Optional> = O extends UnindexableTypesForPointer
? UnindexablePointer
: unknown extends O
? UnindexablePointer
: O extends (infer T)[]
? Pointer<T>[]
: O extends {}
? {
[K in keyof O]-?: Pointer<O[K] | Optional>
}
: UnindexablePointer
2021-06-18 13:05:06 +02:00
const pointerMetaSymbol = Symbol('pointerMeta')
const proxyHandler = {
get(
pointerKey: WeakPointerKey,
prop: string | typeof pointerMetaSymbol,
): $IntentionalAny {
if (prop === pointerMetaSymbol) return pointerMetaWeakMap.get(pointerKey)!
let subPathPointers = cachedSubPathPointersWeakMap.get(pointerKey)
if (!subPathPointers) {
subPathPointers = new Map()
cachedSubPathPointersWeakMap.set(pointerKey, subPathPointers)
2021-06-18 13:05:06 +02:00
}
const existing = subPathPointers.get(prop)
if (existing !== undefined) return existing
2021-06-18 13:05:06 +02:00
const meta = pointerMetaWeakMap.get(pointerKey)!
2021-06-18 13:05:06 +02:00
const subPointer = pointer({root: meta.root, path: [...meta.path, prop]})
subPathPointers.set(prop, subPointer)
2021-06-18 13:05:06 +02:00
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
*/
export const getPointerMeta = <_>(p: PointerType<_>): PointerMeta => {
2021-06-18 13:05:06 +02:00
// @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.
*/
export const getPointerParts = <_>(
p: Pointer<_>,
2021-06-18 13:05:06 +02:00
): {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 pointerKey: WeakPointerKey = {}
pointerMetaWeakMap.set(pointerKey, meta)
return new Proxy(pointerKey, proxyHandler) as Pointer<O>
2021-06-18 13:05:06 +02:00
}
export default pointer
2022-01-19 13:06:13 +01:00
/**
* Returns whether `p` is a pointer.
*/
export const isPointer = (p: $IntentionalAny): p is Pointer<unknown> => {
return p && !!getPointerMeta(p)
}