Document utilities and remove unused code

This commit is contained in:
Aria Minaei 2022-11-24 13:17:19 +01:00
parent dd585b0790
commit 464ce24923
18 changed files with 170 additions and 124 deletions

View file

@ -1,12 +1,23 @@
import type {
$IntentionalAny,
SerializableMap,
SerializableValue,
} from '@theatre/shared/utils/types'
import type {ObjectAddressKey, ProjectId, SheetId, SheetInstanceId} from './ids' import type {ObjectAddressKey, ProjectId, SheetId, SheetInstanceId} from './ids'
import memoizeFn from './memoizeFn' import memoizeFn from './memoizeFn'
import type {Nominal} from './Nominal' import type {Nominal} from './Nominal'
/**
* Addresses are used to identify projects, sheets, objects, and other things.
*
* For example, a project's address looks like `{projectId: 'my-project'}`, and a sheet's
* address looks like `{projectId: 'my-project', sheetId: 'my-sheet'}`.
*
* As you see, a Sheet's address is a superset of a Project's address. This is so that we can
* use the same address type for both. All addresses follow the same rule. An object's address
* extends its sheet's address, which extends its project's address.
*
* For example, generating an object's address from a sheet's address is as simple as `{...sheetAddress, objectId: 'my-object'}`.
*
* Also, if you need the projectAddress of an object, you can just re-use the object's address:
* `aFunctionThatRequiresProjectAddress(objectAddress)`.
*/
/** /**
* Represents the address to a project * Represents the address to a project
*/ */
@ -23,6 +34,8 @@ export interface ProjectAddress {
* sheet.address.sheetId === 'a sheet' * sheet.address.sheetId === 'a sheet'
* sheet.address.sheetInstanceId === 'sheetInstanceId' * sheet.address.sheetInstanceId === 'sheetInstanceId'
* ``` * ```
*
* See {@link WithoutSheetInstance} for a type that doesn't include the sheet instance id.
*/ */
export interface SheetAddress extends ProjectAddress { export interface SheetAddress extends ProjectAddress {
sheetId: SheetId sheetId: SheetId
@ -31,7 +44,9 @@ export interface SheetAddress extends ProjectAddress {
/** /**
* Removes `sheetInstanceId` from an address, making it refer to * Removes `sheetInstanceId` from an address, making it refer to
* all instances of a certain `sheetId` * all instances of a certain `sheetId`.
*
* See {@link SheetAddress} for a type that includes the sheet instance id.
*/ */
export type WithoutSheetInstance<T extends SheetAddress> = Omit< export type WithoutSheetInstance<T extends SheetAddress> = Omit<
T, T,
@ -42,7 +57,10 @@ export type SheetInstanceOptional<T extends SheetAddress> =
WithoutSheetInstance<T> & {sheetInstanceId?: SheetInstanceId | undefined} WithoutSheetInstance<T> & {sheetInstanceId?: SheetInstanceId | undefined}
/** /**
* Represents the address to a Sheet's Object * Represents the address to a Sheet's Object.
*
* It includes the sheetInstance, so it's specific to a single instance of a sheet. If you
* would like an address that doesn't include the sheetInstance, use `WithoutSheetInstance<SheetObjectAddress>`.
*/ */
export interface SheetObjectAddress extends SheetAddress { export interface SheetObjectAddress extends SheetAddress {
/** /**
@ -57,15 +75,32 @@ export interface SheetObjectAddress extends SheetAddress {
objectKey: ObjectAddressKey objectKey: ObjectAddressKey
} }
/**
* This is a simple array representing the path to a prop, without specifying the object.
*/
export type PathToProp = Array<string | number> export type PathToProp = Array<string | number>
/**
* Just like {@link PathToProp}, but encoded as a string. Since this type is nominal,
* it can only be generated using {@link encodePathToProp}.
*/
export type PathToProp_Encoded = Nominal<'PathToProp_Encoded'> export type PathToProp_Encoded = Nominal<'PathToProp_Encoded'>
/**
* Encodes a {@link PathToProp} as a string, and caches the result, so as long
* as the input is the same, the output won't have to be re-generated.
*/
export const encodePathToProp = memoizeFn( export const encodePathToProp = memoizeFn(
(p: PathToProp): PathToProp_Encoded => (p: PathToProp): PathToProp_Encoded =>
// we're using JSON.stringify here, but we could use a faster alternative.
// If you happen to do that, first make sure no `PathToProp_Encoded` is ever
// used in the store, otherwise you'll have to write a migration.
JSON.stringify(p) as PathToProp_Encoded, JSON.stringify(p) as PathToProp_Encoded,
) )
/**
* The decoder of {@link encodePathToProp}.
*/
export const decodePathToProp = (s: PathToProp_Encoded): PathToProp => export const decodePathToProp = (s: PathToProp_Encoded): PathToProp =>
JSON.parse(s) JSON.parse(s)
@ -76,52 +111,35 @@ export interface PropAddress extends SheetObjectAddress {
pathToProp: PathToProp pathToProp: PathToProp
} }
/**
* Represents the address of a certain sequence of a sheet.
*
* Since currently sheets are single-sequence only, `sequenceName` is always `'default'` for now.
*/
export interface SequenceAddress extends SheetAddress { export interface SequenceAddress extends SheetAddress {
sequenceName: string sequenceName: string
} }
export const getValueByPropPath = ( /**
pathToProp: PathToProp, * Returns true if `path` starts with `pathPrefix`.
rootVal: SerializableMap, *
): undefined | SerializableValue => { * Example:
const p = [...pathToProp] * ```ts
let cur: $IntentionalAny = rootVal * const prefix: PathToProp = ['a', 'b']
* console.log(doesPathStartWith(['a', 'b', 'c'], prefix)) // true
while (p.length !== 0) { * console.log(doesPathStartWith(['x', 'b', 'c'], prefix)) // false
const key = p.shift()! * ```
*/
if (cur !== null && typeof cur === 'object') { export function doesPathStartWith(path: PathToProp, pathPrefix: PathToProp) {
if (Array.isArray(cur)) {
if (typeof key === 'number') {
cur = cur[key]
} else {
return undefined
}
} else {
if (typeof key === 'string') {
cur = cur[key]
} else {
return undefined
}
}
} else {
return undefined
}
}
return cur
}
export function doesPathStartWith(
path: (string | number)[],
pathPrefix: (string | number)[],
) {
return pathPrefix.every((pathPart, i) => pathPart === path[i]) return pathPrefix.every((pathPart, i) => pathPart === path[i])
} }
/**
* Returns true if pathToPropA and pathToPropB are equal.
*/
export function arePathsEqual( export function arePathsEqual(
pathToPropA: (string | number)[], pathToPropA: PathToProp,
pathToPropB: (string | number)[], pathToPropB: PathToProp,
) { ) {
if (pathToPropA.length !== pathToPropB.length) return false if (pathToPropA.length !== pathToPropB.length) return false
for (let i = 0; i < pathToPropA.length; i++) { for (let i = 0; i < pathToPropA.length; i++) {
@ -131,7 +149,9 @@ export function arePathsEqual(
} }
/** /**
* e.g. * Given an array of `PathToProp`s, returns the longest common prefix.
*
* Example
* ``` * ```
* commonRootOfPathsToProps([ * commonRootOfPathsToProps([
* ['a','b','c','d','e'], * ['a','b','c','d','e'],
@ -140,8 +160,8 @@ export function arePathsEqual(
* ]) // = ['a','b'] * ]) // = ['a','b']
* ``` * ```
*/ */
export function commonRootOfPathsToProps(pathsToProps: (string | number)[][]) { export function commonRootOfPathsToProps(pathsToProps: PathToProp[]) {
const commonPathToProp: (string | number)[] = [] const commonPathToProp: PathToProp = []
while (true) { while (true) {
const i = commonPathToProp.length const i = commonPathToProp.length
let candidatePathPart = pathsToProps[0]?.[i] let candidatePathPart = pathsToProps[0]?.[i]

View file

@ -1,24 +0,0 @@
export default function createWeakCache(): WeakCache {
const cache = new Map()
const cleanup = new FinalizationRegistry((key) => {
const ref = cache.get(key)
if (ref && !ref.deref()) cache.delete(key)
})
return function getOrSet<T extends {}>(key: string, producer: () => T): T {
const ref = cache.get(key)
if (ref) {
const cached = ref.deref()
if (cached !== undefined) return cached
}
const fresh = producer()
cache.set(key, new WeakRef(fresh))
cleanup.register(fresh, key)
return fresh
}
}
export interface WeakCache {
<T extends {}>(key: string, producer: () => T): T
}

View file

@ -1,20 +0,0 @@
import type {DeepPartialOfSerializableValue, SerializableMap} from './types'
export default function deepMerge<T extends SerializableMap>(
base: T,
override: DeepPartialOfSerializableValue<T>,
): T {
const merged = {...base}
for (const key of Object.keys(override)) {
const valueInOverride = override[key]
const valueInBase = base[key]
// @ts-ignore @todo
merged[key] =
typeof valueInOverride === 'object' && typeof valueInBase === 'object'
? deepMerge(valueInBase, valueInOverride)
: valueInOverride
}
return merged
}

View file

@ -39,7 +39,7 @@ import type {DeepPartialOfSerializableValue, SerializableMap} from './types'
* *
* 4. Both `base` and `override` must be plain JSON values and *NO* arrays, so: `boolean, string, number, undefined, {}` * 4. Both `base` and `override` must be plain JSON values and *NO* arrays, so: `boolean, string, number, undefined, {}`
* *
* Rationale: This is used in {@link SheetObject.getValues()} to deep-merge static and sequenced * Rationale: This is used in {@link SheetObject.getValues} to deep-merge static and sequenced
* and other types of overrides. If we were to do a deep-merge without a cache, we'd be creating and discarding * and other types of overrides. If we were to do a deep-merge without a cache, we'd be creating and discarding
* several JS objects on each frame for every Theatre object, and that would pressure the GC. * several JS objects on each frame for every Theatre object, and that would pressure the GC.
* Plus, keeping the values referentially stable helps lib authors optimize how they patch these values * Plus, keeping the values referentially stable helps lib authors optimize how they patch these values

View file

@ -5,6 +5,28 @@ export interface Deferred<PromiseType> {
status: 'pending' | 'resolved' | 'rejected' status: 'pending' | 'resolved' | 'rejected'
} }
/**
* A simple imperative API for resolving/rejecting a promise.
*
* Example:
* ```ts
* function doSomethingAsync() {
* const deferred = defer()
*
* setTimeout(() => {
* if (Math.random() > 0.5) {
* deferred.resolve('success')
* } else {
* deferred.reject('Something went wrong')
* }
* }, 1000)
*
* // we're just returning the promise, so that the caller cannot resolve/reject it
* return deferred.promise
* }
*
* ```
*/
export function defer<PromiseType>(): Deferred<PromiseType> { export function defer<PromiseType>(): Deferred<PromiseType> {
let resolve: (d: PromiseType) => void let resolve: (d: PromiseType) => void
let reject: (d: unknown) => void let reject: (d: unknown) => void

View file

@ -1,5 +1,13 @@
import propose from 'propose' import propose from 'propose'
/**
* Proposes a suggestion to fix a typo in `str`, using the options provided in `dictionary`.
*
* Example:
* ```ts
* didYouMean('helo', ['hello', 'world']) // 'Did you mean "hello"?'
* ```
*/
export default function didYouMean( export default function didYouMean(
str: string, str: string,
dictionary: string[], dictionary: string[],

View file

@ -1,3 +1,11 @@
/**
* Truncates a string to a given length, adding an ellipsis if it was truncated.
* Example:
* ```ts
* ellipsify('hello world', 5) // 'hello...'
* ellipsify('hello world', 100) // 'hello world'
* ```
*/
export default function ellipsify(str: string, maxLength: number) { export default function ellipsify(str: string, maxLength: number) {
if (str.length <= maxLength) return str if (str.length <= maxLength) return str
return str.substr(0, maxLength - 3) + '...' return str.substr(0, maxLength - 3) + '...'

View file

@ -1,3 +1,10 @@
/**
* All errors thrown to end-users should be an instance of this class.
*/
export class TheatreError extends Error {} export class TheatreError extends Error {}
/**
* If an end-user provided an invalid argument to a public API, the error thrown
* should be an instance of this class.
*/
export class InvalidArgumentError extends TheatreError {} export class InvalidArgumentError extends TheatreError {}

View file

@ -1,6 +1,29 @@
import type {PathToProp} from './addresses' import type {PathToProp} from './addresses'
import type {$IntentionalAny, SerializableMap} from './types' import type {$IntentionalAny, SerializableMap} from './types'
/**
* Iterates recursively over all props of an object (which should be a {@link SerializableMap}) and runs `fn`
* on each prop that has a primitive value (string/number/boolean) and is _NOT_ null/undefined.
*
* Example:
* ```ts
* forEachDeep(
* // The object to iterate over. The `fn` is going to be called on `b` and `c`.
* {a: {b: 1, c: 2, d: null, e: undefined}},
* // the function to run on each prop
* (value, pathToValue) => {
* console.log(value, pathToValue)
* },
* // We can optionally pass a path prefix to prepend to the path of each prop
* ['foo', 'bar'])
*
* // The above will log:
* // 1 ['foo', 'bar', 'a', 'b']
* // 2 ['foo', 'bar', 'a', 'c']
* // Note that null and undefined values are skipped.
* // Also note that `a` is also skippped, because it's not a primitive value.
* ```
*/
export default function forEachDeep< export default function forEachDeep<
Primitive extends string | number | boolean, Primitive extends string | number | boolean,
>( >(

View file

@ -2,6 +2,18 @@ import lodashGet from 'lodash-es/get'
import type {PathToProp} from './addresses' import type {PathToProp} from './addresses'
import type {SerializableValue} from './types' import type {SerializableValue} from './types'
/**
* Returns the value at `path` of `v`.
*
* Example:
* ```ts
* getDeep({a: {b: 1}}, ['a', 'b']) // 1
* getDeep({a: {b: 1}}, ['a', 'c']) // undefined
* getDeep({a: {b: 1}}, []) // {a: {b: 1}}
* getDeep('hello', []) // 'hello''
* getDeep('hello', ['a']) // undefined
* ```
*/
export default function getDeep( export default function getDeep(
v: SerializableValue, v: SerializableValue,
path: PathToProp, path: PathToProp,

View file

@ -1,16 +0,0 @@
import type {$FixMe} from './types'
const getPropsInCommon = (a: $FixMe, b: $FixMe): (string | number)[] => {
const keysInA = Object.keys(a)
const inCommon: (string | number)[] = []
for (const key of keysInA) {
if (b.hasOwnProperty(key)) {
inCommon.push(key)
}
}
return inCommon
}
export default getPropsInCommon

View file

@ -1,3 +0,0 @@
export default function identity<T>(a: T) {
return a
}

View file

@ -1,6 +1,11 @@
import type {VoidFn} from './types' /**
* This is just an empty object used in place of `{}` when you want to:
export const voidFn: VoidFn = () => {} * 1. Not create many new objects (less GC pressure)
* 2. Have the empty object be a singleton (so that `===` works), so it can be fed to memoized functions.
*/
export const emptyObject = {} export const emptyObject = {}
/**
* The array equivalent of {@link emptyObject}.
*/
export const emptyArray: ReadonlyArray<unknown> = [] export const emptyArray: ReadonlyArray<unknown> = []

View file

@ -1,5 +1,7 @@
/** /**
* Memoizes a unary function using a simple weakmap. * Memoizes a unary function using a simple weakmap. The argument to the unary
* function must be WeakCache-able, which means it must be an object and not a plain
* number/string/boolean/etc.
* *
* @example * @example
* ```ts * ```ts

View file

@ -21,10 +21,6 @@ function typeOfValue(v: unknown): ValueType {
} }
} }
/**
* @remarks
* TODO explain what this does.
*/
export default function minimalOverride<T>(base: T, override: T): T { export default function minimalOverride<T>(base: T, override: T): T {
const typeofOverride = typeOfValue(override) const typeofOverride = typeOfValue(override)
if (typeofOverride === ValueType.Opaque) { if (typeofOverride === ValueType.Opaque) {

View file

@ -7,6 +7,9 @@ export type ReduxReducer<State extends {}> = (
export type VoidFn = () => void export type VoidFn = () => void
/**
* A `SerializableMap` is a plain JS object that can be safely serialized to JSON.
*/
export type SerializableMap< export type SerializableMap<
Primitives extends SerializablePrimitive = SerializablePrimitive, Primitives extends SerializablePrimitive = SerializablePrimitive,
> = {[Key in string]?: SerializableValue<Primitives>} > = {[Key in string]?: SerializableValue<Primitives>}

View file

@ -1,4 +1,4 @@
import {voidFn} from '@theatre/shared/utils' import noop from '@theatre/shared/utils/noop'
import React, {createContext, useCallback, useContext, useRef} from 'react' import React, {createContext, useCallback, useContext, useRef} from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import {zIndexes} from './SequenceEditorPanel' import {zIndexes} from './SequenceEditorPanel'
@ -22,7 +22,7 @@ const Container = styled.div`
type ReceiveVerticalWheelEventFn = (ev: Pick<WheelEvent, 'deltaY'>) => void type ReceiveVerticalWheelEventFn = (ev: Pick<WheelEvent, 'deltaY'>) => void
const ctx = createContext<ReceiveVerticalWheelEventFn>(voidFn) const ctx = createContext<ReceiveVerticalWheelEventFn>(noop)
/** /**
* See {@link VerticalScrollContainer} and references for how to use this. * See {@link VerticalScrollContainer} and references for how to use this.

View file

@ -1,6 +1,9 @@
import identity from '@theatre/shared/utils/identity'
import type {$IntentionalAny} from '@theatre/shared/utils/types' import type {$IntentionalAny} from '@theatre/shared/utils/types'
function identity<T>(a: T) {
return a
}
interface Transformer< interface Transformer<
Input extends $IntentionalAny, Input extends $IntentionalAny,
Output extends $IntentionalAny, Output extends $IntentionalAny,