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 = {
/**
* 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',
}

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.
*/
// --
export const isProject = typeAsserter<Project>('Theatre_Project')
export const isSheet = typeAsserter<Sheet>('Theatre_Sheet')

View file

@ -1,9 +1,20 @@
import type {$IntentionalAny} from './types'
/**
* A basic cache
*/
export default class SimpleCache {
/**
* NOTE this could also be a Map.
*/
protected _values: Record<string, unknown> = {}
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 {
if (this.has(key)) {
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 {
return this._values.hasOwnProperty(key)
}

View file

@ -1,9 +1,17 @@
import type {$IntentionalAny} from './types'
/**
* A wrapper around a WeakMap that adds a convenient `getOrSet` method.
*/
export default class WeakMapWithGetOrSet<
K extends object = {},
V = any,
> 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 {
if (this.has(key)) {
return this.get(key) as $IntentionalAny

View file

@ -2,7 +2,7 @@ import padEnd from 'lodash-es/padEnd'
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),
* and we want to avoid setting it to weird value like `101.1239293814314`, when we know that the user
* probably just meant `100`.

View file

@ -2,6 +2,9 @@ import type {Pointer} from '@theatre/dataverse'
import type {PathToProp} from './addresses'
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>(
base: Pointer<T>,
toAppend: PathToProp,

View file

@ -1,6 +1,18 @@
import type {PathToProp} from './addresses'
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(
base: SerializableMap,
path: PathToProp,
@ -8,36 +20,52 @@ export default function removePathFromObject(
if (typeof base !== 'object' || base === null) return
if (path.length === 0) {
const keys = Object.keys(base)
for (const key of keys) {
for (const key of Object.keys(base)) {
delete base[key]
}
return
}
// if path is ['a', 'b', 'c'], then this will be ['a', 'b']
const keysUpToLastKey = path.slice(0, path.length - 1)
let cur: $IntentionalAny = base
// we use this weakmap to be able to get the parent of a a child object
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) {
const parent = cur
const child = parent[key as $FixMe]
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
} else {
// the path _does_ exist so far. let's note the parent-child relationship.
childToParentMapping.set(child, parent)
cur = child
}
}
// if path is ['a', 'b', 'c'], then this will be ['c', 'b', 'a']
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) {
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)!
continue
} else {
return
}
}
}

View file

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

View file

@ -3,7 +3,7 @@ import {InvalidArgumentError} from '@theatre/shared/utils/errors'
const _validateSym = (
val: string,
thingy: string,
thingy: string, // there are two unsolved problems in computer science: cache invalidation and naming things.
range: [min: number, max: number],
): void | 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 = (
name: 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 = (
name: string,
thingy: string,

View file

@ -1,10 +1,16 @@
import type {PathToProp} from './addresses'
import type {$IntentionalAny} from './types'
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>(
state: S,
path: (string | number)[],
obj: S,
path: PathToProp,
replace: $IntentionalAny,
): 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.
*
* 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 =>
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(
unsanitisedPath: string,
fnName: string,

View file

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

View file

@ -1,13 +1,22 @@
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>(
state: S,
obj: S,
path: (string | number | undefined)[],
reducer: (...args: $IntentionalAny[]) => $IntentionalAny,
): S {
if (path.length === 0) return reducer(state)
return hoop(state, path as $IntentionalAny, reducer)
if (path.length === 0) return reducer(obj)
return hoop(obj, path as $IntentionalAny, reducer)
}
const hoop = (

View file

@ -1,5 +1,20 @@
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 => {
if (typeof v === 'string') {
return `string("${ellipsify(v, 10)}")`