diff --git a/theatre/shared/src/globals.ts b/theatre/shared/src/globals.ts index cbeb706..365f9bd 100644 --- a/theatre/shared/src/globals.ts +++ b/theatre/shared/src/globals.ts @@ -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', } diff --git a/theatre/shared/src/instanceTypes.ts b/theatre/shared/src/instanceTypes.ts index ad30c34..8861ec5 100644 --- a/theatre/shared/src/instanceTypes.ts +++ b/theatre/shared/src/instanceTypes.ts @@ -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('Theatre_Project') export const isSheet = typeAsserter('Theatre_Sheet') diff --git a/theatre/shared/src/utils/SimpleCache.ts b/theatre/shared/src/utils/SimpleCache.ts index 29913b1..6a06633 100644 --- a/theatre/shared/src/utils/SimpleCache.ts +++ b/theatre/shared/src/utils/SimpleCache.ts @@ -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 = {} 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(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) } diff --git a/theatre/shared/src/utils/WeakMapWithGetOrSet.ts b/theatre/shared/src/utils/WeakMapWithGetOrSet.ts index 0e8d09f..3437499 100644 --- a/theatre/shared/src/utils/WeakMapWithGetOrSet.ts +++ b/theatre/shared/src/utils/WeakMapWithGetOrSet.ts @@ -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 { + /** + * 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(key: K, producer: () => T): T { if (this.has(key)) { return this.get(key) as $IntentionalAny diff --git a/theatre/shared/src/utils/niceNumberUtils.ts b/theatre/shared/src/utils/niceNumberUtils.ts index 89d00f8..c9e26bf 100644 --- a/theatre/shared/src/utils/niceNumberUtils.ts +++ b/theatre/shared/src/utils/niceNumberUtils.ts @@ -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`. diff --git a/theatre/shared/src/utils/pointerDeep.ts b/theatre/shared/src/utils/pointerDeep.ts index 4369b9d..c862329 100644 --- a/theatre/shared/src/utils/pointerDeep.ts +++ b/theatre/shared/src/utils/pointerDeep.ts @@ -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( base: Pointer, toAppend: PathToProp, diff --git a/theatre/shared/src/utils/removePathFromObject.ts b/theatre/shared/src/utils/removePathFromObject.ts index 31ff1cf..778deaa 100644 --- a/theatre/shared/src/utils/removePathFromObject.ts +++ b/theatre/shared/src/utils/removePathFromObject.ts @@ -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 } } } diff --git a/theatre/shared/src/utils/resolvedPromise.ts b/theatre/shared/src/utils/resolvedPromise.ts index 4ac641d..5ed3078 100644 --- a/theatre/shared/src/utils/resolvedPromise.ts +++ b/theatre/shared/src/utils/resolvedPromise.ts @@ -1,2 +1,5 @@ +/** + * A resolved promise + */ const resolvedPromise = new Promise((r) => r()) export default resolvedPromise diff --git a/theatre/shared/src/utils/sanitizers.ts b/theatre/shared/src/utils/sanitizers.ts index d4d3399..066a45e 100644 --- a/theatre/shared/src/utils/sanitizers.ts +++ b/theatre/shared/src/utils/sanitizers.ts @@ -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, diff --git a/theatre/shared/src/utils/setDeepImmutable.ts b/theatre/shared/src/utils/setDeepImmutable.ts index 2f4b440..1ef8ddf 100644 --- a/theatre/shared/src/utils/setDeepImmutable.ts +++ b/theatre/shared/src/utils/setDeepImmutable.ts @@ -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( - 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 } diff --git a/theatre/shared/src/utils/slashedPaths.ts b/theatre/shared/src/utils/slashedPaths.ts index 3c3c9de..9bfc54d 100644 --- a/theatre/shared/src/utils/slashedPaths.ts +++ b/theatre/shared/src/utils/slashedPaths.ts @@ -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, diff --git a/theatre/shared/src/utils/transformNumber.ts b/theatre/shared/src/utils/transformNumber.ts index 4a68efd..4ddcc45 100644 --- a/theatre/shared/src/utils/transformNumber.ts +++ b/theatre/shared/src/utils/transformNumber.ts @@ -1,3 +1,6 @@ +/** + * Transforms `initialPos` by scaling and translating it around `origin`. + */ export function transformNumber( initialPos: number, { diff --git a/theatre/shared/src/utils/updateDeep.ts b/theatre/shared/src/utils/updateDeep.ts index 62503ad..8491e19 100644 --- a/theatre/shared/src/utils/updateDeep.ts +++ b/theatre/shared/src/utils/updateDeep.ts @@ -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( - 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 = ( diff --git a/theatre/shared/src/utils/userReadableTypeOfValue.ts b/theatre/shared/src/utils/userReadableTypeOfValue.ts index 5afacd5..260db3a 100644 --- a/theatre/shared/src/utils/userReadableTypeOfValue.ts +++ b/theatre/shared/src/utils/userReadableTypeOfValue.ts @@ -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)}")`