Document several util functions
This commit is contained in:
parent
7899f8a965
commit
f17ad3cbca
14 changed files with 132 additions and 17 deletions
|
@ -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',
|
||||
}
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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`.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
/**
|
||||
* A resolved promise
|
||||
*/
|
||||
const resolvedPromise = new Promise<void>((r) => r())
|
||||
export default resolvedPromise
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
/**
|
||||
* Transforms `initialPos` by scaling and translating it around `origin`.
|
||||
*/
|
||||
export function transformNumber(
|
||||
initialPos: number,
|
||||
{
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -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)}")`
|
||||
|
|
Loading…
Reference in a new issue