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 = {
|
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',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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`.
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
/**
|
||||||
|
* Transforms `initialPos` by scaling and translating it around `origin`.
|
||||||
|
*/
|
||||||
export function transformNumber(
|
export function transformNumber(
|
||||||
initialPos: number,
|
initialPos: number,
|
||||||
{
|
{
|
||||||
|
|
|
@ -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 = (
|
||||||
|
|
|
@ -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)}")`
|
||||||
|
|
Loading…
Reference in a new issue