diff --git a/theatre/shared/src/utils/addresses.ts b/theatre/shared/src/utils/addresses.ts index 17827c2..a4040ae 100644 --- a/theatre/shared/src/utils/addresses.ts +++ b/theatre/shared/src/utils/addresses.ts @@ -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 = Omit< T, @@ -42,7 +57,10 @@ export type SheetInstanceOptional = WithoutSheetInstance & {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`. */ 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 +/** + * 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] diff --git a/theatre/shared/src/utils/createWeakCache.ts b/theatre/shared/src/utils/createWeakCache.ts deleted file mode 100644 index a12398c..0000000 --- a/theatre/shared/src/utils/createWeakCache.ts +++ /dev/null @@ -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(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 { - (key: string, producer: () => T): T -} diff --git a/theatre/shared/src/utils/deepMerge.ts b/theatre/shared/src/utils/deepMerge.ts deleted file mode 100644 index 15f7a97..0000000 --- a/theatre/shared/src/utils/deepMerge.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type {DeepPartialOfSerializableValue, SerializableMap} from './types' - -export default function deepMerge( - base: T, - override: DeepPartialOfSerializableValue, -): 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 -} diff --git a/theatre/shared/src/utils/deepMergeWithCache.ts b/theatre/shared/src/utils/deepMergeWithCache.ts index 601fcc7..a859ecf 100644 --- a/theatre/shared/src/utils/deepMergeWithCache.ts +++ b/theatre/shared/src/utils/deepMergeWithCache.ts @@ -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 diff --git a/theatre/shared/src/utils/defer.ts b/theatre/shared/src/utils/defer.ts index 983ecf4..8abf22f 100644 --- a/theatre/shared/src/utils/defer.ts +++ b/theatre/shared/src/utils/defer.ts @@ -5,6 +5,28 @@ export interface Deferred { 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(): Deferred { let resolve: (d: PromiseType) => void let reject: (d: unknown) => void diff --git a/theatre/shared/src/utils/didYouMean.ts b/theatre/shared/src/utils/didYouMean.ts index 146d03b..92e47be 100644 --- a/theatre/shared/src/utils/didYouMean.ts +++ b/theatre/shared/src/utils/didYouMean.ts @@ -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[], diff --git a/theatre/shared/src/utils/ellipsify.ts b/theatre/shared/src/utils/ellipsify.ts index cbaf36d..4949481 100644 --- a/theatre/shared/src/utils/ellipsify.ts +++ b/theatre/shared/src/utils/ellipsify.ts @@ -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) + '...' diff --git a/theatre/shared/src/utils/errors.ts b/theatre/shared/src/utils/errors.ts index f265fa8..00b6f99 100644 --- a/theatre/shared/src/utils/errors.ts +++ b/theatre/shared/src/utils/errors.ts @@ -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 {} diff --git a/theatre/shared/src/utils/forEachDeep.ts b/theatre/shared/src/utils/forEachDeep.ts index be188d1..53f2952 100644 --- a/theatre/shared/src/utils/forEachDeep.ts +++ b/theatre/shared/src/utils/forEachDeep.ts @@ -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, >( diff --git a/theatre/shared/src/utils/getDeep.ts b/theatre/shared/src/utils/getDeep.ts index 14aba08..5c7261d 100644 --- a/theatre/shared/src/utils/getDeep.ts +++ b/theatre/shared/src/utils/getDeep.ts @@ -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, diff --git a/theatre/shared/src/utils/getPropsInCommon.tsx b/theatre/shared/src/utils/getPropsInCommon.tsx deleted file mode 100644 index 65282a4..0000000 --- a/theatre/shared/src/utils/getPropsInCommon.tsx +++ /dev/null @@ -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 diff --git a/theatre/shared/src/utils/identity.ts b/theatre/shared/src/utils/identity.ts deleted file mode 100644 index 136e85c..0000000 --- a/theatre/shared/src/utils/identity.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default function identity(a: T) { - return a -} diff --git a/theatre/shared/src/utils/index.ts b/theatre/shared/src/utils/index.ts index c6ca1a9..7993c1d 100644 --- a/theatre/shared/src/utils/index.ts +++ b/theatre/shared/src/utils/index.ts @@ -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 = [] diff --git a/theatre/shared/src/utils/memoizeFn.ts b/theatre/shared/src/utils/memoizeFn.ts index 70fe471..e7f9f15 100644 --- a/theatre/shared/src/utils/memoizeFn.ts +++ b/theatre/shared/src/utils/memoizeFn.ts @@ -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 diff --git a/theatre/shared/src/utils/minimalOverride.ts b/theatre/shared/src/utils/minimalOverride.ts index 2df3480..7d192b2 100644 --- a/theatre/shared/src/utils/minimalOverride.ts +++ b/theatre/shared/src/utils/minimalOverride.ts @@ -21,10 +21,6 @@ function typeOfValue(v: unknown): ValueType { } } -/** - * @remarks - * TODO explain what this does. - */ export default function minimalOverride(base: T, override: T): T { const typeofOverride = typeOfValue(override) if (typeofOverride === ValueType.Opaque) { diff --git a/theatre/shared/src/utils/types.ts b/theatre/shared/src/utils/types.ts index 927def3..59e4498 100644 --- a/theatre/shared/src/utils/types.ts +++ b/theatre/shared/src/utils/types.ts @@ -7,6 +7,9 @@ export type ReduxReducer = ( 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} diff --git a/theatre/studio/src/panels/SequenceEditorPanel/VerticalScrollContainer.tsx b/theatre/studio/src/panels/SequenceEditorPanel/VerticalScrollContainer.tsx index ff3db57..771303a 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/VerticalScrollContainer.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/VerticalScrollContainer.tsx @@ -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) => void -const ctx = createContext(voidFn) +const ctx = createContext(noop) /** * See {@link VerticalScrollContainer} and references for how to use this. diff --git a/theatre/studio/src/utils/redux/actionCreator.ts b/theatre/studio/src/utils/redux/actionCreator.ts index 05af71c..c447173 100644 --- a/theatre/studio/src/utils/redux/actionCreator.ts +++ b/theatre/studio/src/utils/redux/actionCreator.ts @@ -1,6 +1,9 @@ -import identity from '@theatre/shared/utils/identity' import type {$IntentionalAny} from '@theatre/shared/utils/types' +function identity(a: T) { + return a +} + interface Transformer< Input extends $IntentionalAny, Output extends $IntentionalAny,