UNPKG

web-utility

Version:

Web front-end toolkit based on TypeScript

381 lines (307 loc) 10.1 kB
import { Scalar } from './math'; export type Constructor<T> = new (...args: any[]) => T; export type AbstractClass<T> = abstract new (...args: any[]) => T; export type Values<T> = Required<T>[keyof T]; export type TypeKeys<T, D> = { [K in keyof T]: Required<T>[K] extends D ? K : never; }[keyof T]; export type PickSingle<T> = T extends infer S | (infer S)[] ? S : T; export type PickData<T> = Omit<T, TypeKeys<T, Function>>; export type DataKeys<T> = Exclude<keyof T, TypeKeys<T, Function>>; export function likeNull(value?: any) { return !(value != null) || Number.isNaN(value); } export function isEmpty(value?: any) { return ( likeNull(value) || (typeof value === 'object' ? !Object.keys(value).length : value === '') ); } /** * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toStringTag} */ export const classNameOf = (data: any): string => Object.prototype.toString.call(data).slice(8, -1); export function assertInheritance(Sub: Function, Super: Function) { return Sub.prototype instanceof Super; } export function proxyPrototype<T extends object>( target: T, dataStore: Record<IndexKey, any>, setter?: (key: IndexKey, value: any) => any ) { const prototype = Object.getPrototypeOf(target); const prototypeProxy = new Proxy(prototype, { set: (_, key, value, receiver) => { if (key in receiver) Reflect.set(prototype, key, value, receiver); else dataStore[key] = value; setter?.(key, value); return true; }, get: (prototype, key, receiver) => key in dataStore ? dataStore[key] : Reflect.get(prototype, key, receiver) }); Object.setPrototypeOf(target, prototypeProxy); } export function isUnsafeNumeric(raw: string) { return ( /^[\d.]+$/.test(raw) && raw.localeCompare(Number.MAX_SAFE_INTEGER + '', undefined, { numeric: true }) > 0 ); } export function byteLength(raw: string) { return raw.replace(/[^\u0021-\u007e\uff61-\uffef]/g, 'xx').length; } export type HyphenCase<T extends string> = T extends `${infer L}${infer R}` ? `${L extends Uppercase<L> ? `-${Lowercase<L>}` : L}${HyphenCase<R>}` : T; export function toHyphenCase(raw: string) { return raw.replace( /[A-Z]+|[^A-Za-z][A-Za-z]/g, (match, offset) => `${offset ? '-' : ''}${(match[1] || match[0]).toLowerCase()}` ); } export type CamelCase< Raw extends string, Delimiter extends string = '-' > = Uncapitalize< Raw extends `${infer L}${Delimiter}${infer R}` ? `${Capitalize<L>}${Capitalize<CamelCase<R>>}` : `${Capitalize<Raw>}` >; export function toCamelCase(raw: string, large = false) { return raw.replace(/^[A-Za-z]|[^A-Za-z][A-Za-z]/g, (match, offset) => offset || large ? (match[1] || match[0]).toUpperCase() : match.toLowerCase() ); } export function uniqueID() { return (Date.now() + parseInt((Math.random() + '').slice(2))).toString(36); } export function objectFrom<V, K extends string>(values: V[], keys: K[]) { return Object.fromEntries( values.map((value, index) => [keys[index], value]) ) as Record<K, V>; } export enum DiffStatus { Old = -1, Same = 0, New = 1 } export function diffKeys<T extends IndexKey>(oldList: T[], newList: T[]) { const map = {} as Record<T, DiffStatus>; for (const item of oldList) map[item] = DiffStatus.Old; for (const item of newList) { map[item] ||= 0; map[item] += DiffStatus.New; } return { map, group: groupBy( Object.entries<DiffStatus>(map), ([key, status]) => status ) }; } export type ResultArray<T> = T extends ArrayLike<infer D> ? D[] : T[]; export function likeArray(data?: any): data is ArrayLike<any> { if (likeNull(data)) return false; const { length } = data; return typeof length === 'number' && length >= 0 && ~~length === length; } export type TypedArray = | Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array | BigInt64Array | BigUint64Array; /** * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray} */ export const isTypedArray = (data: any): data is TypedArray => data instanceof Object.getPrototypeOf(Int8Array); export function makeArray<T>(data?: T) { if (data instanceof Array) return data as unknown as ResultArray<T>; if (likeNull(data)) return [] as ResultArray<T>; if (likeArray(data)) return Array.from(data) as ResultArray<T>; return [data] as ResultArray<T>; } export const splitArray = <T>(array: T[], unitLength: number) => array.reduce((grid, item, index) => { (grid[~~(index / unitLength)] ||= [])[index % unitLength] = item; return grid; }, [] as T[][]); export type IndexKey = number | string | symbol; export type GroupKey<T extends Record<IndexKey, any>> = keyof T | IndexKey; export type Iteratee<T extends Record<IndexKey, any>> = | keyof T | ((item: T) => GroupKey<T> | GroupKey<T>[]); export function groupBy<T extends Record<IndexKey, any>>( list: T[], iteratee: Iteratee<T> ) { const data = {} as Record<GroupKey<T>, T[]>; for (const item of list) { let keys = iteratee instanceof Function ? iteratee(item) : item[iteratee]; if (!(keys instanceof Array)) keys = [keys]; for (const key of new Set( (keys as GroupKey<T>[]).filter(key => key != null) )) (data[key] = data[key] || []).push(item); } return data; } export function countBy<T extends Record<IndexKey, any>>( list: T[], iteratee: Iteratee<T> ) { const group = groupBy(list, iteratee); const sortedList = Object.entries(group).map( ([key, { length }]) => [key, length] as const ); return Object.fromEntries(sortedList); } export function findDeep<T>( list: T[], subKey: TypeKeys<Required<T>, any[]>, handler: (item: T) => boolean ): T[] { for (const item of list) { if (handler(item)) return [item]; if (item[subKey] instanceof Array) { const result = findDeep( item[subKey] as unknown as T[], subKey, handler ); if (result.length) return [item, ...result]; } } return []; } export type TreeNode< IK extends string, PK extends string, CK extends string > = { [key in IK]: number | string; } & { [key in PK]?: number | string; } & { [key in CK]?: TreeNode<IK, PK, CK>[]; }; export function treeFrom< IK extends string, PK extends string, CK extends string, N extends TreeNode<IK, PK, CK> >( list: N[], idKey = 'id' as IK, parentIdKey = 'parentId' as PK, childrenKey = 'children' as CK ) { list = globalThis.structuredClone?.(list) || JSON.parse(JSON.stringify(list)); const map: Record<string, N> = {}; const roots: N[] = []; for (const item of list) map[item[idKey] as string] = item; for (const item of list) { const parent = map[item[parentIdKey] as string]; if (!parent) roots.push(item); else { parent[childrenKey] ||= [] as TreeNode<IK, PK, CK>[] as N[CK]; parent[childrenKey].push(item); } } if (!roots[0]) throw new ReferenceError('No root node is found'); return roots; } export function cache<I, O>( executor: (cleaner: () => void, ...data: I[]) => O, title: string ) { var cacheData: O; return function (...data: I[]) { if (cacheData != null) return cacheData; console.trace(`[Cache] execute: ${title}`); cacheData = executor.call( this, (): void => (cacheData = undefined), ...data ); Promise.resolve(cacheData).then( data => console.log(`[Cache] refreshed: ${title} => ${data}`), error => console.error(`[Cache] failed: ${error?.message || error}`) ); return cacheData; }; } export interface IteratorController<V = any, E = Error> { next: (value: V) => any; error: (error: E) => any; complete: () => any; } export async function* createAsyncIterator<V, E = Error>( executor: (controller: IteratorController<V, E>) => (() => any) | void ) { let { promise, resolve, reject } = Promise.withResolvers<V>(); const doneSymbol = Symbol('done'), done = Promise.withResolvers<symbol>(); const disposer = executor({ next: value => resolve(value), error: error => { reject(error); // @ts-ignore disposer?.(); }, complete: () => { done.resolve(doneSymbol); // @ts-ignore disposer?.(); } }); while (true) { const value = await Promise.race([promise, done.promise]); if (value === doneSymbol) return; yield value as V; ({ promise, resolve, reject } = Promise.withResolvers<V>()); } } export async function* mergeStream<T, R = void, N = T>( ...sources: (() => AsyncIterator<T, R, N>)[] ) { var iterators = sources.map(item => item()); while (iterators[0]) { const dones: number[] = []; for ( let i = 0, iterator: AsyncIterator<T>; (iterator = iterators[i]); i++ ) { const { done, value } = await iterator.next(); if (!done) yield value; else dones.push(i); } iterators = iterators.filter((_, i) => !dones.includes(i)); } } export class ByteSize extends Scalar { units = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'].map((name, i) => ({ base: 1024 ** i, name: name + 'B' })); }