/** * Extended array */ /** * Max number of elements passed per function call. * Spreading or applying a large array into a single call * ("fn(...data)") overflows the argument stack on most browsers * (Chrome throws "RangeError: Maximum call stack size exceeded" * at roughly 65k arguments), so bulk operations must be chunked. */ export const MAX_ARG_LENGTH = 30000; /** * Appends the contents of source to target in argument-stack-safe chunks, * the chunk-safe replacement for target.push(...source) * * @param target the array to append to * @param source the elements to append * @returns target for chaining */ export function pushChunked(target: T[], source: ArrayLike): T[] { for (let start = 0, len = source.length; start < len; start += MAX_ARG_LENGTH) { Array.prototype.push.apply(target, Array.prototype.slice.call(source, start, start + MAX_ARG_LENGTH)); } return target; } /** * Extended array which adds various es 2019 shim functions to the normal array * We must remap all array producing functions in order to keep * the delegation active, once we are in! */ class Es2019Array_ extends Array{ _another: T[]; constructor(another: T[] = []) { super(); // species constructors and legacy code paths may pass a scalar another = Array.isArray(another) ? another : [another] as T[]; if((another as any)._another) { this._another = (another as any)._another; } else { this._another = another; } pushChunked(this, another); //for testing it definitely runs into this branch because we are on es5 level //if (!(Array.prototype).flatMap as any) { this.flatMap = (flatMapFun) => this._flatMap(flatMapFun) as any; //} //if (!(Array.prototype).flat as any) { this.flat = (flatLevel: number = 1) => this._flat(flatLevel); //} } map(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[] { const ret = Array.prototype.map.call(this._another, callbackfn, thisArg); return (_Es2019ArrayFromArr as any)(ret); } concat(...items: any[]): T[] { const ret = Array.prototype.concat.apply(this._another, items); return (_Es2019ArrayFromArr as any)(ret); } reverse(): T[] { const ret = Array.prototype.reverse.call(this._another); return (_Es2019ArrayFromArr as any)(ret); } slice(start?: number, end?: number): T[] { const ret = Array.prototype.slice.call(this._another, start, end); return (_Es2019ArrayFromArr as any)(ret); } splice(start: number, deleteCount?: number): T[] { const ret = Array.prototype.splice.call(this._another, start, deleteCount ?? 0); return (_Es2019ArrayFromArr as any)(ret); } filter(predicate: (value: T, index: number, array: T[]) => any, thisArg?: any): S[] { const ret = Array.prototype.filter.call(this._another, predicate, thisArg); return (_Es2019ArrayFromArr as any)(ret); } reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue?: T): T { const ret = Array.prototype.reduce.call(this._another, callbackfn as any, initialValue); return ret as T; } /*reduceRight(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T): T { const ret = Array.prototype.reduceRight.call(callbackfn, initialValue); return ret; }*/ private _flat(flatDepth = 1) { return this._flatResolve(this._another, flatDepth); } private _flatResolve(arr: any[], flatDepth = 1): any[] { //recursion break if (flatDepth == 0) { return arr; } let res: any[] = []; let reFlat = (item: any) => { item = Array.isArray(item) ? item : [item]; let mapped = this._flatResolve(item, flatDepth - 1); res = res.concat(mapped); }; arr.forEach(reFlat) return Es2019ArrayFrom(res); } private _flatMap(mapperFunction: Function): any { let res = this.map(item => mapperFunction(item)); return this._flatResolve(res); } } //let _Es2019Array = function(...data: T[]) {}; //let oldProto = Es2019Array.prototype; export function _Es2019Array(...data: T[]): Es2019Array_ { return _Es2019ArrayFromArr(data); } /** * chunk-safe variant of _Es2019Array which takes the backing array * directly instead of spreading it into the call */ export function _Es2019ArrayFromArr(data: T[]): Es2019Array_ { let ret = new Es2019Array_(data); let proxied = new Proxy>(ret, { get(target: Es2019Array_, p: string | symbol, receiver: any): any { if("symbol" == typeof p) { return (target._another as any)[p]; } if(!isNaN(parseInt(p as string))) { return (target._another as any)[p]; } else { return (target as any)[p]; } }, set(target, property, value): boolean { (target as any)[property] = value; (target._another as any)[property] = value; return true; } }); return proxied; }; /** * this is the switch between normal array and our shim * the shim is only provided in case the native browser * does not yet have flatMap support on arrays */ // Runtime check for browser compatibility — TypeScript knows flatMap exists in lib but older browsers may not have it. interface Es2019ArrayConstructor { new(...data: any[]): T[]; (...data: any[]): T[]; } export var Es2019Array: Es2019ArrayConstructor = (((Array.prototype as any).flatMap) ? function(...data: T[]): T[] { // sometimes the typescript compiler produces // an array without flatmap between boundaries (the result produces True for Array.isArray // but has no flatMap function, could be a node issue also or Typescript! // we remap that (could be related to: https://github.com/microsoft/TypeScript/issues/31033 // the check and remap fixes the issue which should not exist in the first place return (data as any)?.flatMap ? data : _Es2019ArrayFromArr(data); } : _Es2019Array) as Es2019ArrayConstructor; /** * chunk-safe variant of new Es2019Array(...source) - * spreading a large array into the constructor call overflows the * argument stack ("Maximum call stack size exceeded"), this builder * copies the data over in safe chunks instead * * @param source an array or array-like holding the initial data */ export function Es2019ArrayFrom(source: ArrayLike): T[] { const data: T[] = pushChunked([], source); return ((Array.prototype as any).flatMap) ? data : _Es2019ArrayFromArr(data); }