UNPKG

@speckle/shared

Version:

Shared code between various Speckle JS packages

170 lines (147 loc) 4.62 kB
import { isNull, isNumber, isUndefined, noop } from '#lodash' import type { MaybeAsync, NonNullableProperties, NullableKeysToOptional } from './utilityTypes.js' import { ensureError } from './error.js' export class TimeoutError extends Error {} export class WaitIntervalUntilCanceledError extends Error {} /** * Build promise that can be resolved/rejected manually outside of the promise's execution scope */ export const buildManualPromise = <T>() => { let resolve: (value: T) => void // eslint-disable-next-line @typescript-eslint/no-explicit-any let reject: (reason?: any) => void const promise = new Promise<T>((res, rej) => { resolve = res reject = rej }) const resolveWrapper: typeof resolve = (...args) => resolve(...args) const rejectWrapper: typeof reject = (...args) => reject(...args) return { promise, resolve: resolveWrapper, reject: rejectWrapper } } export type ManualPromise<T> = ReturnType<typeof buildManualPromise<T>> export const isNullOrUndefined = (val: unknown): val is null | undefined => isNull(val) || isUndefined(val) export const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) export const waitIntervalUntil = (ms: number, predicate: () => boolean) => { const { promise, resolve, reject } = buildManualPromise<void>() const interval = setInterval(() => { if (predicate()) { clearInterval(interval) resolve() } }, ms) const ret = promise as typeof promise & { cancel: () => void } ret.cancel = () => { clearInterval(interval) reject(new WaitIntervalUntilCanceledError()) } return ret } /** * Not nullable type guard, useful in `.filter()` calls for proper TS typed * results */ export const isNonNullable = <V>(v: V): v is NonNullable<typeof v> => !!v /** * Make the promise throw after enough time has passed. Useful for implementing timeout functionality in various flows. */ export const timeoutAt = (ms: number, optionalMessage?: string) => { // create error beforehand, so we have a better stack trace const err = new TimeoutError(optionalMessage || 'timeoutAt() timed out') return new Promise<never>((_resolve, reject) => setTimeout(() => { reject(err) }, ms) ) } /** * Invoke and return fn(), but retry it up to n times if it throws */ export const retry = async <V = unknown>( fn: () => MaybeAsync<V>, n: number, delayMs?: number | ((attempt: number, error: Error) => number) ) => { let lastError: Error | undefined for (let i = 0; i < n; i++) { try { const res = await Promise.resolve(fn()) return res } catch (error) { lastError = ensureError(error) if (delayMs && i + 1 < n) { if (isNumber(delayMs)) { await wait(delayMs) } else { await wait(delayMs(i + 1, lastError)) } } } } throw lastError || new Error('Unexpected retry() failure') } /** * For quickly profiling a function */ export const profile = async <V = unknown>( fn: () => MaybeAsync<V>, label?: string, extra?: unknown ) => { const start = performance.now() const res = await Promise.resolve(fn()) const end = performance.now() console.log( `[${label || 'profile'}] took ${end - start}ms`, ...(extra ? [extra] : []) ) return res } /** * For quickly profiling a sync function */ export const profileSync = <V = unknown>( fn: () => V, label?: string, extra?: unknown ) => { const start = performance.now() const res = fn() const end = performance.now() console.log( `[${label || 'profile'}] took ${end - start}ms`, ...(extra ? [extra] : []) ) return res } export const removeNullOrUndefinedKeys = <T extends Record<string, unknown>>( obj: T ) => { const ret = {} as T for (const key in obj) { if (!isNullOrUndefined(obj[key])) { ret[key] = obj[key] } } return ret as NonNullableProperties<NullableKeysToOptional<T>> } export const coerceUndefinedValuesToNull = <T extends Record<string, unknown>>( obj: T ) => { const ret = {} as Record<string, Exclude<T[keyof T], undefined> | null> for (const [key, value] of Object.entries(obj)) { ret[key] = isUndefined(value) ? null : (value as Exclude<T[keyof T], undefined>) } return ret } export const isArrayOf = <T>(arr: unknown, guard: (v: unknown) => v is T): arr is T[] => Array.isArray(arr) && arr.every(guard) export const waitForever = (): Promise<never> => new Promise<never>(noop) /** * Returns true if only one of the arguments is truthy */ export const xor = (a: unknown, b: unknown) => !!((a || b) && !(a && b))