Document several util functions

This commit is contained in:
Aria Minaei 2022-11-24 15:41:08 +01:00
parent 7899f8a965
commit f17ad3cbca
14 changed files with 132 additions and 17 deletions

View file

@ -1,4 +1,13 @@
const globals = { const globals = {
/**
* If the schema of the redux store changes in a backwards-incompatible way, then this version number should be incremented.
*
* While this looks like semver, it is not. There are no patch numbers, so any change in this number is a breaking change.
*
* However, as long as the schema of the redux store is backwards-compatible, then we don't have to change this number.
*
* Since the 0.4.0 release, this number has not had to change.
*/
currentProjectStateDefinitionVersion: '0.4.0', currentProjectStateDefinitionVersion: '0.4.0',
} }

View file

@ -13,8 +13,6 @@ import type {$IntentionalAny} from './utils/types'
* The functions in this module are supposed to be a replacement for that. * The functions in this module are supposed to be a replacement for that.
*/ */
// --
export const isProject = typeAsserter<Project>('Theatre_Project') export const isProject = typeAsserter<Project>('Theatre_Project')
export const isSheet = typeAsserter<Sheet>('Theatre_Sheet') export const isSheet = typeAsserter<Sheet>('Theatre_Sheet')

View file

@ -1,9 +1,20 @@
import type {$IntentionalAny} from './types' import type {$IntentionalAny} from './types'
/**
* A basic cache
*/
export default class SimpleCache { export default class SimpleCache {
/**
* NOTE this could also be a Map.
*/
protected _values: Record<string, unknown> = {} protected _values: Record<string, unknown> = {}
constructor() {} constructor() {}
/**
* get the cache item at `key` or produce it using `producer` and cache _that_.
*
* Note that this won't work if you change the producer, like `get(key, producer1); get(key, producer2)`.
*/
get<T>(key: string, producer: () => T): T { get<T>(key: string, producer: () => T): T {
if (this.has(key)) { if (this.has(key)) {
return this._values[key] as $IntentionalAny return this._values[key] as $IntentionalAny
@ -14,6 +25,9 @@ export default class SimpleCache {
} }
} }
/**
* Returns true if the cache has an item at `key`.
*/
has(key: string): boolean { has(key: string): boolean {
return this._values.hasOwnProperty(key) return this._values.hasOwnProperty(key)
} }

View file

@ -1,9 +1,17 @@
import type {$IntentionalAny} from './types' import type {$IntentionalAny} from './types'
/**
* A wrapper around a WeakMap that adds a convenient `getOrSet` method.
*/
export default class WeakMapWithGetOrSet< export default class WeakMapWithGetOrSet<
K extends object = {}, K extends object = {},
V = any, V = any,
> extends WeakMap<K, V> { > extends WeakMap<K, V> {
/**
* get the cache item at `key` or produce it using `producer` and cache _that_.
*
* Note that this won't work if you change the producer, like `getOrSet(key, producer1); getOrSet(key, producer2)`.
*/
getOrSet<T extends V>(key: K, producer: () => T): T { getOrSet<T extends V>(key: K, producer: () => T): T {
if (this.has(key)) { if (this.has(key)) {
return this.get(key) as $IntentionalAny return this.get(key) as $IntentionalAny

View file

@ -2,7 +2,7 @@ import padEnd from 'lodash-es/padEnd'
import logger from '@theatre/shared/logger' import logger from '@theatre/shared/logger'
/** /**
* Returns the _aesthetically pleasing_ (aka "nicest") number `c`, such that `a <= c <= b`. * Returns the _most aesthetically pleasing_ (aka "nicest") number `c`, such that `a <= c <= b`.
* This is useful when a numeric value is being "nudged" by the user (e.g. dragged via mouse pointer), * This is useful when a numeric value is being "nudged" by the user (e.g. dragged via mouse pointer),
* and we want to avoid setting it to weird value like `101.1239293814314`, when we know that the user * and we want to avoid setting it to weird value like `101.1239293814314`, when we know that the user
* probably just meant `100`. * probably just meant `100`.

View file

@ -2,6 +2,9 @@ import type {Pointer} from '@theatre/dataverse'
import type {PathToProp} from './addresses' import type {PathToProp} from './addresses'
import type {$IntentionalAny} from './types' import type {$IntentionalAny} from './types'
/**
* Points deep into a pointer, using `toAppend` as the path. This is _NOT_ type-safe, so use with caution.
*/
export default function pointerDeep<T>( export default function pointerDeep<T>(
base: Pointer<T>, base: Pointer<T>,
toAppend: PathToProp, toAppend: PathToProp,

View file

@ -1,6 +1,18 @@
import type {PathToProp} from './addresses' import type {PathToProp} from './addresses'
import type {$FixMe, $IntentionalAny, SerializableMap} from './types' import type {$FixMe, $IntentionalAny, SerializableMap} from './types'
/**
* Mutates `base` to remove the path `path` from it. And if deleting a key makes
* its parent object empty of keys, that parent object will be deleted too, and so on.
*
* If `path` is `[]`, then `base` it'll remove all props from `base`.
*
* Example:
* ```ts
* removePathFromObject({a: {b: 1, c: 2}}, ['a', 'b']) // base is mutated to: {a: {c: 2}}
* removePathFromObject({a: {b: 1}}, ['a', 'b']) // base is mutated to: {}, because base.a is now empty.
* ```
*/
export default function removePathFromObject( export default function removePathFromObject(
base: SerializableMap, base: SerializableMap,
path: PathToProp, path: PathToProp,
@ -8,36 +20,52 @@ export default function removePathFromObject(
if (typeof base !== 'object' || base === null) return if (typeof base !== 'object' || base === null) return
if (path.length === 0) { if (path.length === 0) {
const keys = Object.keys(base) for (const key of Object.keys(base)) {
for (const key of keys) {
delete base[key] delete base[key]
} }
return return
} }
// if path is ['a', 'b', 'c'], then this will be ['a', 'b']
const keysUpToLastKey = path.slice(0, path.length - 1) const keysUpToLastKey = path.slice(0, path.length - 1)
let cur: $IntentionalAny = base let cur: $IntentionalAny = base
// we use this weakmap to be able to get the parent of a a child object
const childToParentMapping = new WeakMap() const childToParentMapping = new WeakMap()
// The algorithm has two passes.
// On the first pass, we traverse the path and keep note of parent->child relationships.
// We also can bail out early if we find that the path doesn't exist.
for (const key of keysUpToLastKey) { for (const key of keysUpToLastKey) {
const parent = cur const parent = cur
const child = parent[key as $FixMe] const child = parent[key as $FixMe]
if (typeof child !== 'object' || child === null) { if (typeof child !== 'object' || child === null) {
// the path either doesn't exist, or it doesn't point to an object, so we can just return
return return
} else { } else {
// the path _does_ exist so far. let's note the parent-child relationship.
childToParentMapping.set(child, parent) childToParentMapping.set(child, parent)
cur = child cur = child
} }
} }
// if path is ['a', 'b', 'c'], then this will be ['c', 'b', 'a']
const keysReversed = path.slice().reverse() const keysReversed = path.slice().reverse()
// on the second pass, we traverse the path in reverse, and delete the keys,
// and also delete the parent objects if they become empty.
for (const key of keysReversed) { for (const key of keysReversed) {
delete cur[key] delete cur[key]
if (Object.keys(cur).length === 0) {
// if the current object is _not_ empty, then we can stop here.
if (Object.keys(cur).length > 0) {
return
} else {
// otherwise, we need to delete the parent object too.
cur = childToParentMapping.get(cur)! cur = childToParentMapping.get(cur)!
continue continue
} else {
return
} }
} }
} }

View file

@ -1,2 +1,5 @@
/**
* A resolved promise
*/
const resolvedPromise = new Promise<void>((r) => r()) const resolvedPromise = new Promise<void>((r) => r())
export default resolvedPromise export default resolvedPromise

View file

@ -3,7 +3,7 @@ import {InvalidArgumentError} from '@theatre/shared/utils/errors'
const _validateSym = ( const _validateSym = (
val: string, val: string,
thingy: string, thingy: string, // there are two unsolved problems in computer science: cache invalidation and naming things.
range: [min: number, max: number], range: [min: number, max: number],
): void | string => { ): void | string => {
if (typeof val !== 'string') { if (typeof val !== 'string') {
@ -15,6 +15,12 @@ const _validateSym = (
} }
} }
/**
* Validates a name, so that:
* - It's a string
* - It doesn't have leading or trailing spaces
* - It's between 3 and 32 characters long
*/
export const validateName = ( export const validateName = (
name: string, name: string,
thingy: string, thingy: string,
@ -28,6 +34,12 @@ export const validateName = (
} }
} }
/**
* Validates an instanceId, so that:
* - It's a string
* - It doesn't have leading or trailing spaces
* - It's between 1 and 32 characters long
*/
export const validateInstanceId = ( export const validateInstanceId = (
name: string, name: string,
thingy: string, thingy: string,

View file

@ -1,10 +1,16 @@
import type {PathToProp} from './addresses'
import type {$IntentionalAny} from './types' import type {$IntentionalAny} from './types'
import updateImmutable from './updateDeep' import updateImmutable from './updateDeep'
/**
* Returns an immutable clone of `obj`, with its `path` replaced with `replace`. This is _NOT_ type-safe, so use with caution.
*
* TODO Make a type-safe version of this, like ./mutableSetDeep.ts.
*/
export default function setDeepImmutable<S>( export default function setDeepImmutable<S>(
state: S, obj: S,
path: (string | number)[], path: PathToProp,
replace: $IntentionalAny, replace: $IntentionalAny,
): S { ): S {
return updateImmutable(state, path, () => replace) as $IntentionalAny as S return updateImmutable(obj, path, () => replace) as $IntentionalAny as S
} }

View file

@ -4,7 +4,9 @@ import {notify} from '@theatre/shared/notify'
/** /**
* Make the given string's "path" slashes normalized with preceding and trailing spaces. * Make the given string's "path" slashes normalized with preceding and trailing spaces.
* *
* Prev "sanifySlashedPath". * - It removes starting and trailing slashes: `/foo/bar/` becomes `foo / bar`
* - It adds wraps each slash with a single space, so that `foo/bar` becomes `foo / bar`
*
*/ */
const normalizeSlashedPath = (p: string): string => const normalizeSlashedPath = (p: string): string =>
p p
@ -29,6 +31,11 @@ const getValidationErrorsOfSlashedPath = (p: string): void | string => {
} }
} }
/**
* Sanitizes a `path` and warns the user if the input doesn't match the sanitized output.
*
* See {@link normalizeSlashedPath} for examples of how we do sanitization.
*/
export function validateAndSanitiseSlashedPathOrThrow( export function validateAndSanitiseSlashedPathOrThrow(
unsanitisedPath: string, unsanitisedPath: string,
fnName: string, fnName: string,

View file

@ -1,3 +1,6 @@
/**
* Transforms `initialPos` by scaling and translating it around `origin`.
*/
export function transformNumber( export function transformNumber(
initialPos: number, initialPos: number,
{ {

View file

@ -1,13 +1,22 @@
import type {$FixMe, $IntentionalAny} from './types' import type {$FixMe, $IntentionalAny} from './types'
/* eslint-disable */ /**
* Returns a new object with the value at `path` replaced with the result of `reducer(oldValue)`.
*
* Example:
* ```ts
* updateDeep({a: {b: 1}}, ['a', 'b'], (x) => x + 1) // {a: {b: 2}}
* updateDeep({a: {b: 1}}, [], (x) => Object.keys(x).length) // 1
* updateDeep({a: {b: 1}}, ['a', 'c'], (x) => (x ?? 0) + 1) // {a: {b: 1, c: 1}}
* ```
*/
export default function updateDeep<S>( export default function updateDeep<S>(
state: S, obj: S,
path: (string | number | undefined)[], path: (string | number | undefined)[],
reducer: (...args: $IntentionalAny[]) => $IntentionalAny, reducer: (...args: $IntentionalAny[]) => $IntentionalAny,
): S { ): S {
if (path.length === 0) return reducer(state) if (path.length === 0) return reducer(obj)
return hoop(state, path as $IntentionalAny, reducer) return hoop(obj, path as $IntentionalAny, reducer)
} }
const hoop = ( const hoop = (

View file

@ -1,5 +1,20 @@
import ellipsify from './ellipsify' import ellipsify from './ellipsify'
/**
* Returns a short, user-readable description of the type of `value`.
* Examples:
* ```ts
* userReadableTypeOfValue(1) // 'number(1)'
* userReadableTypeOfValue(12345678901112) // 'number(1234567...)'
* userReadableTypeOfValue('hello') // 'string("hello")'
* userReadableTypeOfValue('hello world this is a long string') // 'string("hello wo...")'
* userReadableTypeOfValue({a: 1, b: 2}) // 'object'
* userReadableTypeOfValue([1, 2, 3]) // 'array'
* userReadableTypeOfValue(null) // 'null'
* userReadableTypeOfValue(undefined) // 'undefined'
* userReadableTypeOfValue(true) // 'true'
* ```
*/
const userReadableTypeOfValue = (v: unknown): string => { const userReadableTypeOfValue = (v: unknown): string => {
if (typeof v === 'string') { if (typeof v === 'string') {
return `string("${ellipsify(v, 10)}")` return `string("${ellipsify(v, 10)}")`