@deepkit/core
Version:
Deepkit core library
648 lines (569 loc) • 15.7 kB
text/typescript
/*
* Deepkit Framework
* Copyright (C) 2021 Deepkit UG, Marc J. Schmidt
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the MIT License.
*
* You should have received a copy of the MIT License along with this program.
*/
import dotProp from 'dot-prop';
import { eachPair } from './iterators';
/**
* Makes sure the error once printed using console.log contains the actual class name.
*
* @example
* ```
* class MyApiError extends CustomerError {}
*
* throw MyApiError() // prints MyApiError instead of simply "Error".
* ```
*
* @public
*/
export class CustomError extends Error {
public name: string;
public stack?: string;
constructor(public message: string = '') {
super(message);
this.name = this.constructor.name;
}
}
/**
* @public
*/
export interface ClassType<T = any> {
new(...args: any[]): T;
}
/**
* @public
*/
export type AbstractClassType<T = any> = abstract new (...args: any[]) => T;
export type ExtractClassType<T> = T extends ClassType<infer K> ? K : never;
declare const __forward: unique symbol;
/**
* This type maintains the actual type, but erases the decoratorMetadata, which is requires in a circular reference for ECMAScript modules.
* Basically fixes like "ReferenceError: Cannot access 'MyClass' before initialization"
*/
export type Forward<T> = T & { [__forward]?: true };
/**
* Returns the class name either of the class definition or of the class of an instance.
*
* Note when code is minimized/uglified this output will change. You should disable in your compile the
* className modification.
*
* @example
* ```typescript
* class User {}
*
* expect(getClassName(User)).toBe('User');
* expect(getClassName(new User())).toBe('User');
* ```
*
* @public
*/
export function getClassName<T>(classTypeOrInstance: ClassType<T> | Object): string {
if (!classTypeOrInstance) return 'undefined';
const proto = (classTypeOrInstance as any)['prototype'] ? (classTypeOrInstance as any)['prototype'] : classTypeOrInstance;
return proto.constructor.name;
}
/**
* Same as getClassName but appends the propertyName.
* @public
*/
export function getClassPropertyName<T>(classType: ClassType<T> | Object, propertyName: string): string {
const name = getClassName(classType);
return `${name}.${propertyName}`;
}
/**
* @public
*/
export function applyDefaults<T>(classType: ClassType<T>, target: { [k: string]: any }): T {
const classInstance = new classType();
for (const [i, v] of eachPair(target)) {
(classInstance as any)[i] = v;
}
return classInstance;
}
/**
* Tries to identify the object by normalised result of Object.toString(obj).
*/
export function typeOf(obj: any) {
return ((({}).toString.call(obj).match(/\s([a-zA-Z]+)/) || [])[1] || '').toLowerCase();
}
/**
* Returns true if the given obj is a plain object, and no class instance.
*
* isPlainObject(\{\}) === true
* isPlainObject(new ClassXY) === false
*
* @public
*/
export function isPlainObject(obj: any): obj is object {
return Boolean(obj && typeof obj === 'object' && obj.constructor instanceof obj.constructor);
}
/**
* Returns the ClassType for a given instance.
*/
export function getClassTypeFromInstance<T>(target: T): ClassType<T> {
if (!isClassInstance(target)) {
throw new Error(`Value is not a class instance. Got ${stringifyValueWithType(target)}`);
}
return (target as any)['constructor'] as ClassType<T>;
}
/**
* Returns true when target is a class instance.
*/
export function isClassInstance(target: any): boolean {
return target !== undefined && target !== null
&& target['constructor']
&& Object.getPrototypeOf(target) === (target as any)['constructor'].prototype
&& !isPlainObject(target)
&& isObject(target);
}
/**
* Returns a human readable string representation from the given value.
*/
export function stringifyValueWithType(value: any): string {
if ('string' === typeof value) return `String(${value})`;
if ('number' === typeof value) return `Number(${value})`;
if ('boolean' === typeof value) return `Boolean(${value})`;
if ('function' === typeof value) return `Function ${value.name}`;
if (isPlainObject(value)) return `Object ${prettyPrintObject(value)}`;
if (isObject(value)) return `${getClassName(getClassTypeFromInstance(value))} ${prettyPrintObject(value)}`;
if (null === value) return `null`;
return 'undefined';
}
/**
* Changes the class of a given instance and returns the new object.
*
* @example
* ```typescript
*
* class Model1 {
* id: number = 0;
* }
*
* class Model2 {
* id: number = 0;
* }
*
* const model1 = new Model1();
* const model2 = changeClass(model1, Model2);
* model2 instanceof Model2; //true
* ```
*/
export function changeClass<T>(value: any, newClass: ClassType<T>): T {
return Object.assign(Object.create(newClass.prototype), value);
}
export function prettyPrintObject(object: object): string {
let res: string[] = [];
for (const i in object) {
res.push(i + ': ' + stringifyValueWithType((object as any)[i]));
}
return '{' + res.join(',') + '}';
}
/**
* Returns true if given obj is a function.
*
* @public
*/
export function isFunction(obj: any): obj is Function {
if ('function' === typeof obj) {
return !obj.toString().startsWith('class ');
}
return false;
}
const AsyncFunction = (async () => {
}).constructor;
/**
* Returns true if given obj is a async function.
*
* @public
*/
export function isAsyncFunction(obj: any): obj is (...args: any[]) => Promise<any> {
return obj instanceof AsyncFunction;
}
/**
* Returns true if given obj is a promise like object.
*
* Note: There's not way to check if it's actually a Promise using instanceof since
* there are a lot of different implementations around.
*
* @public
*/
export function isPromise<T>(obj: any | Promise<T>): obj is Promise<T> {
return obj !== null && typeof obj === 'object' && typeof obj.then === 'function'
&& typeof obj.catch === 'function' && typeof obj.finally === 'function';
}
/**
* Returns true if given obj is a ES6 class (ES5 fake classes are not supported).
*
* @public
*/
export function isClass(obj: any): obj is ClassType {
if ('function' === typeof obj) {
return obj.toString().startsWith('class ') || obj.toString().startsWith('class{');
}
return false;
}
/**
* Returns true for real objects: object literals ({}) or class instances (new MyClass).
*
* @public
*/
export function isObject(obj: any): obj is { [key: string]: any } {
if (obj === null) {
return false;
}
return (typeof obj === 'object' && !isArray(obj));
}
/**
* @public
*/
export function isArray(obj: any): obj is any[] {
return !!(obj && 'number' === typeof obj.length && 'function' === typeof obj.reduce);
}
/**
* @public
*/
export function isNull(obj: any): obj is null {
return null === obj;
}
/**
* @public
*/
export function isUndefined(obj: any): obj is undefined {
return undefined === obj;
}
/**
* Checks if obj is not null and not undefined.
*
* @public
*/
export function isSet(obj: any): boolean {
return !isNull(obj) && !isUndefined(obj);
}
/**
* @public
*/
export function isNumber(obj: any): obj is number {
return 'number' === typeOf(obj);
}
/**
* @public
*/
export function isString(obj: any): obj is string {
return 'string' === typeOf(obj);
}
/**
* @public
*/
export function indexOf<T>(array: T[], item: T): number {
if (!array) {
return -1;
}
return array.indexOf(item);
}
/**
* @public
*/
export async function sleep(seconds: number): Promise<void> {
return new Promise<void>(resolve => setTimeout(resolve, seconds * 1000));
}
/**
* Creates a shallow copy of given array.
*
* @public
*/
export function copy<T>(v: T[]): T[] {
if (isArray(v)) {
return v.slice(0);
}
return v;
}
/**
* Checks whether given array or object is empty (no keys). If given object is falsy, returns false.
*
* @public
*/
export function empty<T>(value?: T[] | object | {}): boolean {
if (!value) return true;
if (isArray(value)) {
return value.length === 0;
} else {
for (const i in value) if (value.hasOwnProperty(i)) return false;
return true;
}
}
/**
* Returns the size of given array or object.
*
* @public
*/
export function size<T>(array: T[] | { [key: string]: T }): number {
if (!array) {
return 0;
}
if (isArray(array)) {
return array.length;
} else {
return getObjectKeysSize(array);
}
}
/**
* Returns the first key of a given object.
*
* @public
*/
export function firstKey(v: { [key: string]: any } | object): string | undefined {
return Object.keys(v)[0];
}
/**
* Returns the last key of a given object.
*
* @public
*/
export function lastKey(v: { [key: string]: any } | object): string | undefined {
const keys = Object.keys(v);
if (keys.length) {
return;
}
return keys[keys.length - 1];
}
/**
* Returns the first value of given array or object.
*
* @public
*/
export function first<T>(v: { [key: string]: T } | T[]): T | undefined {
if (isArray(v)) {
return v[0];
}
const key = firstKey(v);
if (key) {
return v[key];
}
return;
}
/**
* Returns the last value of given array or object.
*
* @public
*/
export function last<T>(v: { [key: string]: T } | T[]): T | undefined {
if (isArray(v)) {
if (v.length > 0) {
return v[v.length - 1];
}
return;
}
const key = firstKey(v);
if (key) {
return v[key];
}
return;
}
/**
* Returns the average of a number array.
*
* @public
*/
export function average(array: number[]): number {
let sum = 0;
for (const n of array) {
sum += n;
}
return sum / array.length;
}
/**
* @public
*/
export function prependObjectKeys(o: { [k: string]: any }, prependText: string): { [k: string]: any } {
const converted: { [k: string]: any } = {};
for (const i in o) {
if (!o.hasOwnProperty(i)) continue;
converted[prependText + i] = o[i];
}
return converted;
}
/**
* @public
*/
export function appendObject(origin: { [k: string]: any }, extend: { [k: string]: any }, prependKeyName: string = '') {
const no = prependObjectKeys(extend, prependKeyName);
for (const [i, v] of eachPair(no)) {
origin[i] = v;
}
}
/**
* A better alternative to "new Promise()" that supports error handling and maintains the stack trace for Error.stack.
*
* When you use `new Promise()` you need to wrap your code inside a try-catch to call `reject` on error.
* asyncOperation() does this automatically.
*
* When you use `new Promise()` you will lose the stack trace when `reject(new Error())` is called.
* asyncOperation() makes sure the error stack trace is the correct one.
*
* @example
* ```typescript
* await asyncOperation(async (resolve, reject) => {
* await doSomething(); //if this fails, reject() will automatically be called
* stream.on('data', (data) => {
* resolve(data); //at some point you MUST call resolve(data)
* });
* });
* ```
*
* @public
*/
export async function asyncOperation<T>(executor: (resolve: (value: T) => void, reject: (error: any) => void) => void | Promise<void>): Promise<T> {
try {
return await new Promise<T>(async (resolve, reject) => {
try {
await executor(resolve, reject);
} catch (e) {
reject(e);
}
});
} catch (error) {
mergeStack(error, createStack());
throw error;
}
}
/**
* @public
*/
export function mergePromiseStack<T>(promise: Promise<T>, stack?: string): Promise<T> {
stack = stack || createStack();
promise.then(() => {
}, (error) => {
mergeStack(error, stack || '');
});
return promise;
}
/**
* @beta
*/
export function createStack(removeCallee: boolean = true): string {
if (Error.stackTraceLimit === 10) Error.stackTraceLimit = 100;
let stack = new Error().stack || '';
/*
at createStack (/file/path)
at promiseToObservable (/file/path)
at userLandCode1 (/file/path)
at userLandCode2 (/file/path)
*/
//remove "at createStack"
stack = stack.slice(stack.indexOf(' at ') + 6);
stack = stack.slice(stack.indexOf(' at ') - 1);
if (removeCallee) {
//remove callee
stack = stack.slice(stack.indexOf(' at ') + 6);
stack = stack.slice(stack.indexOf(' at ') - 1);
}
return stack;
}
/**
* @beta
*/
export function mergeStack(error: Error, stack: string) {
if (error instanceof Error && error.stack) {
error.stack += '\n' + stack;
}
}
export function collectForMicrotask<T>(callback: (args: T[]) => void): (arg: T) => void {
let items: T[] = [];
let taskScheduled = false;
return (arg: T) => {
items.push(arg);
if (!taskScheduled) {
taskScheduled = true;
queueMicrotask(() => {
taskScheduled = false;
callback(items);
items.length = 0;
});
}
};
}
/**
* Returns the current time as seconds.
*
* @public
*/
export function time(): number {
return Date.now() / 1000;
}
/**
* @public
*/
export function getPathValue(bag: { [field: string]: any }, parameterPath: string, defaultValue?: any): any {
if (isSet(bag[parameterPath])) {
return bag[parameterPath];
}
const result = dotProp.get(bag, parameterPath);
return isSet(result) ? result : defaultValue;
}
/**
* @public
*/
export function setPathValue(bag: object, parameterPath: string, value: any) {
dotProp.set(bag, parameterPath, value);
}
/**
* @public
*/
export function deletePathValue(bag: object, parameterPath: string) {
dotProp.delete(bag, parameterPath);
}
/**
* Returns the human readable byte representation.
*
* @public
*/
export function humanBytes(bytes: number, si: boolean = false): string {
const thresh = si ? 1000 : 1024;
if (Math.abs(bytes) < thresh) {
return bytes + ' B';
}
const units = si
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
let u = -1;
do {
bytes /= thresh;
++u;
} while (Math.abs(bytes) >= thresh && u < units.length - 1);
return bytes.toFixed(2) + ' ' + units[u];
}
/**
* Returns the number of properties on `obj`. This is 20x faster than Object.keys(obj).length.
*/
export function getObjectKeysSize(obj: object): number {
let size = 0;
for (let i in obj) if (obj.hasOwnProperty(i)) size++;
return size;
}
export function isConstructable(fn: any): boolean {
try {
new new Proxy(fn, { construct: () => ({}) });
return true;
} catch (err) {
return false;
}
};
export function isPrototypeOfBase(prototype: ClassType | undefined, base: ClassType): boolean {
if (!prototype) return false;
if (prototype === base) return true;
let currentProto = Object.getPrototypeOf(prototype);
while (currentProto && currentProto !== Object.prototype) {
if (currentProto === base) return true;
currentProto = Object.getPrototypeOf(currentProto);
}
return false;
}
declare var v8debug: any;
export function inDebugMode() {
return typeof v8debug === 'object' || /--debug|--inspect/.test(process.execArgv.join(' '));
}