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 memoizeFn from './memoizeFn'
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
*/
@ -23,6 +34,8 @@ export interface ProjectAddress {
* sheet.address.sheetId === 'a sheet'
* sheet.address.sheetInstanceId === 'sheetInstanceId'
* ```
*
* See {@link WithoutSheetInstance} for a type that doesn't include the sheet instance id.
*/
export interface SheetAddress extends ProjectAddress {
sheetId: SheetId
@ -31,7 +44,9 @@ export interface SheetAddress extends ProjectAddress {
/**
* 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<
T,
@ -42,7 +57,10 @@ export type SheetInstanceOptional<T extends SheetAddress> =
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 {
/**
@ -57,15 +75,32 @@ export interface SheetObjectAddress extends SheetAddress {
objectKey: ObjectAddressKey
}
/**
* This is a simple array representing the path to a prop, without specifying the object.
*/
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'>
/**
* 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(
(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,
)
/**
* The decoder of {@link encodePathToProp}.
*/
export const decodePathToProp = (s: PathToProp_Encoded): PathToProp =>
JSON.parse(s)
@ -76,52 +111,35 @@ export interface PropAddress extends SheetObjectAddress {
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 {
sequenceName: string
}
export const getValueByPropPath = (
pathToProp: PathToProp,
rootVal: SerializableMap,
): undefined | SerializableValue => {
const p = [...pathToProp]
let cur: $IntentionalAny = rootVal
while (p.length !== 0) {
const key = p.shift()!
if (cur !== null && typeof cur === 'object') {
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)[],
) {
/**
* Returns true if `path` starts with `pathPrefix`.
*
* Example:
* ```ts
* const prefix: PathToProp = ['a', 'b']
* console.log(doesPathStartWith(['a', 'b', 'c'], prefix)) // true
* console.log(doesPathStartWith(['x', 'b', 'c'], prefix)) // false
* ```
*/
export function doesPathStartWith(path: PathToProp, pathPrefix: PathToProp) {
return pathPrefix.every((pathPart, i) => pathPart === path[i])
}
/**
* Returns true if pathToPropA and pathToPropB are equal.
*/
export function arePathsEqual(
pathToPropA: (string | number)[],
pathToPropB: (string | number)[],
pathToPropA: PathToProp,
pathToPropB: PathToProp,
) {
if (pathToPropA.length !== pathToPropB.length) return false
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([
* ['a','b','c','d','e'],
@ -140,8 +160,8 @@ export function arePathsEqual(
* ]) // = ['a','b']
* ```
*/
export function commonRootOfPathsToProps(pathsToProps: (string | number)[][]) {
const commonPathToProp: (string | number)[] = []
export function commonRootOfPathsToProps(pathsToProps: PathToProp[]) {
const commonPathToProp: PathToProp = []
while (true) {
const i = commonPathToProp.length
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, {}`
*
* 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
* 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

View file

@ -5,6 +5,28 @@ export interface Deferred<PromiseType> {
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> {
let resolve: (d: PromiseType) => void
let reject: (d: unknown) => void

View file

@ -1,5 +1,13 @@
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(
str: 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) {
if (str.length <= maxLength) return str
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 {}
/**
* 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 {}

View file

@ -1,6 +1,29 @@
import type {PathToProp} from './addresses'
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<
Primitive extends string | number | boolean,
>(

View file

@ -2,6 +2,18 @@ import lodashGet from 'lodash-es/get'
import type {PathToProp} from './addresses'
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(
v: SerializableValue,
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'
export const voidFn: VoidFn = () => {}
/**
* This is just an empty object used in place of `{}` when you want to:
* 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 = {}
/**
* The array equivalent of {@link emptyObject}.
*/
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
* ```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 {
const typeofOverride = typeOfValue(override)
if (typeofOverride === ValueType.Opaque) {

View file

@ -7,6 +7,9 @@ export type ReduxReducer<State extends {}> = (
export type VoidFn = () => void
/**
* A `SerializableMap` is a plain JS object that can be safely serialized to JSON.
*/
export type SerializableMap<
Primitives extends SerializablePrimitive = SerializablePrimitive,
> = {[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 styled from 'styled-components'
import {zIndexes} from './SequenceEditorPanel'
@ -22,7 +22,7 @@ const Container = styled.div`
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.

View file

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