UNPKG

inventoresed

Version:

Z-Wave driver written entirely in JavaScript/TypeScript

211 lines (195 loc) 5.84 kB
import { isArray, isObject } from "alcalzone-shared/typeguards"; import { num2hex } from "./strings"; /** Object.keys, but with `(keyof T)[]` as the return type */ export function keysOf<T>(obj: T): (keyof T)[] { return Object.keys(obj) as unknown as (keyof T)[]; } /** Returns a subset of `obj` that contains only the given keys */ export function pick<T extends Record<any, any>, K extends keyof T>( obj: T, keys: readonly K[], ): Pick<T, K> { const ret = {} as Pick<T, K>; for (const key of keys) { if (key in obj) ret[key] = obj[key]; } return ret; } /** * Traverses an object and returns the property identified by the given path. For example, picking from * ```json * { * "foo": { * "bar": [ * 1, 2, 3 * ] * } * ``` * with path `foo.bar.1` will return `2`. */ export function pickDeep<T = unknown>( object: Record<string, any>, path: string, ): T { function _pickDeep(obj: Record<string, any>, pathArr: string[]): unknown { // are we there yet? then return obj if (!pathArr.length) return obj; // are we not looking at an object or array? Then bail if (!isObject(obj) && !isArray(obj)) return undefined; // go deeper const propName = pathArr.shift()!; return _pickDeep(obj[propName], pathArr); } return _pickDeep(object, path.split(".")) as T; } /** Calls the map function of the given array and flattens the result by one level */ export function flatMap<U, T extends any[]>( array: T[], callbackfn: (value: T, index: number, array: T[]) => U[], ): U[] { const mapped = array.map(callbackfn); return mapped.reduce((acc, cur) => [...acc, ...cur], [] as U[]); } /** * Returns a human-readable representation of the given enum value. * If the given value is not found in the enum object, `"unknown (<value-as-hex>)"` is returned. * * @param enumeration The enumeration object the value comes from * @param value The enum value to be pretty-printed */ export function getEnumMemberName(enumeration: unknown, value: number): string { return (enumeration as any)[value] || `unknown (${num2hex(value)})`; } /** Skips the first n bytes of a buffer and returns the rest */ export function skipBytes(buf: Buffer, n: number): Buffer { return Buffer.from(buf.slice(n)); } /** * Returns a throttled version of the given function. No matter how often the throttled version is called, * the underlying function is only called at maximum every `intervalMs` milliseconds. */ export function throttle<T extends any[]>( fn: (...args: T) => void, intervalMs: number, trailing: boolean = false, ): (...args: T) => void { let lastCall = 0; let timeout: NodeJS.Timeout | undefined; return (...args: T) => { const now = Date.now(); if (now >= lastCall + intervalMs) { // waited long enough, call now lastCall = now; fn(...args); } else if (trailing) { if (timeout) clearTimeout(timeout); const delay = lastCall + intervalMs - now; timeout = setTimeout(() => { lastCall = now; fn(...args); }, delay); } }; } /** * Merges the user-defined options with the default options */ export function mergeDeep( target: Record<string, any> | undefined, source: Record<string, any>, ): Record<string, any> { target = target || {}; for (const [key, value] of Object.entries(source)) { if (!(key in target)) { target[key] = value; } else { if (typeof value === "object") { // merge objects target[key] = mergeDeep(target[key], value); } else if (typeof target[key] === "undefined") { // don't override single keys target[key] = value; } } } return target; } /** * Creates a deep copy of the given object */ export function cloneDeep<T>(source: T): T { if (isArray(source)) { return source.map((i) => cloneDeep(i)) as any; } else if (isObject(source)) { const target: any = {}; for (const [key, value] of Object.entries(source)) { target[key] = cloneDeep(value); } return target; } else { return source; } } /** Pads a firmware version string, so it can be compared with semver */ export function padVersion(version: string): string { if (version.split(".").length === 3) return version; return version + ".0"; } /** * Using a binary search, this finds the highest discrete value in [rangeMin...rangeMax] where executor returns true, assuming that * increasing the value will at some point cause the executor to return false. */ export async function discreteBinarySearch( rangeMin: number, rangeMax: number, executor: (value: number) => boolean | PromiseLike<boolean>, ): Promise<number | undefined> { let min = rangeMin; let max = rangeMax; while (min < max) { const mid = min + Math.floor((max - min + 1) / 2); const result = await executor(mid); if (result) { min = mid; } else { max = mid - 1; } } if (min === rangeMin) { // We didn't test this yet const result = await executor(min); if (!result) return undefined; } return min; } /** * Using a linear search, this finds the highest discrete value in [rangeMin...rangeMax] where executor returns true, assuming that * increasing the value will at some point cause the executor to return false. */ export async function discreteLinearSearch( rangeMin: number, rangeMax: number, executor: (value: number) => boolean | PromiseLike<boolean>, ): Promise<number | undefined> { for (let val = rangeMin; val <= rangeMax; val++) { const result = await executor(val); if (!result) { // Found the first value where it no longer returns true if (val === rangeMin) { // No success at all break; } else { // The previous test was successful return val - 1; } } else { if (val === rangeMax) { // Everything was successful return rangeMax; } } } } export function sum(values: number[]): number { return values.reduce((acc, cur) => acc + cur, 0); }