@aedart/support
Version:
The Ion support package
296 lines (281 loc) • 8.61 kB
JavaScript
import { isArrayLike as isArrayLike$1 } from 'lodash-es';
import { TYPED_ARRAY_PROTOTYPE } from '@aedart/contracts/support/reflections';
import { populate } from '@aedart/support/objects';
import { getErrorMessage, configureCustomError } from '@aedart/support/exceptions';
import { ArrayMergeError as ArrayMergeError$1 } from '@aedart/support/arrays';
/**
* @aedart/support
*
* BSD-3-Clause, Copyright (c) 2023-present Alin Eugen Deac <aedart@gmail.com>.
*/
/**
* Determine if array includes all given values
*
* @param {any[]} arr
* @param {any[]} values
*
* @return {boolean}
*/
function includesAll(arr, /* eslint-disable-line @typescript-eslint/no-explicit-any */ values /* eslint-disable-line @typescript-eslint/no-explicit-any */) {
return values.every((value) => arr.includes(value));
}
/**
* Determine if array includes any (_some_) of the given values
*
* @param {any[]} arr
* @param {any[]} values
*
* @return {boolean}
*/
function includesAny(arr, /* eslint-disable-line @typescript-eslint/no-explicit-any */ values /* eslint-disable-line @typescript-eslint/no-explicit-any */) {
return values.some((value) => arr.includes(value));
}
/**
* Determine if value is "array-like".
* (Alias for Lodash's [`isArrayLike`]{@link import('lodash').isArrayLike}) method.
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array#array-like_objects
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Indexed_collections#working_with_array-like_objects
*
* @param {any} value
*
* @return {boolean}
*/
const isArrayLike = isArrayLike$1;
/**
* Determine if target object contains the well-known symbol {@link Symbol.isConcatSpreadable}
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/isConcatSpreadable
*
* @param {object} target
*
* @return {boolean}
*/
function isConcatSpreadable(target) {
return typeof target == 'object'
&& target !== null
&& Reflect.has(target, Symbol.isConcatSpreadable);
}
/**
* Determine if given target is an instance of a `TypedArray`
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray
*
* @param {object} target
*
* @return {boolean}
*/
function isTypedArray(target) {
return target instanceof TYPED_ARRAY_PROTOTYPE;
}
/**
* Determine if value is "safe" array-like object
*
* **Note**: _In this context "safe" means that given object passes {@link isArrayLike},
* but value is:_
* - not a string.
* - not instance of a {@link String} object.
* - not a [Typed Array]{@link isTypedArray} object.
*
* @param {object} value
*
* @return {boolean}
*/
function isSafeArrayLike(value /* eslint-disable-line @typescript-eslint/no-explicit-any */) {
return typeof value != 'string'
&& !(value instanceof String)
&& !isTypedArray(value)
&& isArrayLike(value);
}
/**
* Default Array Merge callback
*
* @param {any} element
* @param {number} index
* @param {any[]} array
* @param {Readonly<ArrayMergeOptions>} options
*
* @return {any}
*/
const defaultArrayMergeCallback = function (element, /* eslint-disable-line @typescript-eslint/no-explicit-any */ index, array, /* eslint-disable-line @typescript-eslint/no-explicit-any */ options) {
// Transfer function, if requested by options.
if (options.transferFunctions) {
return element;
}
// Otherwise, create a structured clone of the element
return structuredClone(element);
};
/**
* Default Array Merge Options
*/
class DefaultArrayMergeOptions {
/**
* Transfer functions
*
* **When `true`**: _functions are transferred into resulting array._
*
* **When `false` (_default behaviour_)**: _The merge operation will fail when a function
* is encountered (functions are not cloneable by default)._
*
* @type {boolean}
*/
transferFunctions = false;
/**
* Merge callback to be applied
*
* **Note**: _When no callback is provided, then the merge function's default
* callback is used._
*/
callback;
/**
* Create new default merge options from given options
*
* @param {ArrayMergeCallback | ArrayMergeOptions} [options]
*/
constructor(options) {
// Merge provided options, if any given
if (options && typeof options == 'object') {
populate(this, options);
}
// Resolve merge callback
this.callback = (options && typeof options == 'function')
? options
: defaultArrayMergeCallback;
}
/**
* Create new default merge options from given options
*
* @param {ArrayMergeOptions} [options]
*
* @return {Readonly<DefaultArrayMergeOptions|ArrayMergeOptions>}
*/
static from(options) {
const resolved = new this(options);
return Object.freeze(resolved);
}
}
/**
* Array Merger
*/
class Merger {
/**
* Merge options to be applied
*
* @type {Readonly<DefaultArrayMergeOptions | ArrayMergeOptions>}
*
* @protected
*/
_options;
/**
* Create new Array Merger instance
*
* @param {ArrayMergeCallback | ArrayMergeOptions} [options]
*/
constructor(options) {
// @ts-expect-error Need to init options, however they are resolved via "using".
this._options = null;
this.using(options);
}
/**
* Use the following merge options
*
* @param {ArrayMergeCallback | ArrayMergeOptions} [options]
*
* @return {this}
*
* @throws {ArrayMergeException}
*/
using(options) {
this._options = this.resolveOptions(options);
return this;
}
/**
* Merge options to be applied
*
* @type {Readonly<DefaultArrayMergeOptions | ArrayMergeOptions>}
*/
get options() {
return this._options;
}
/**
* Returns a merger of given source arrays
*
* @param {...any[]} sources
*
* @return {any[]}
*
* @throws {ArrayMergeException}
*/
of(...sources) {
try {
const options = this.options;
const callback = options.callback.bind(this);
// Array.concat only performs shallow copies of the array values, which might
// fine in some situations. However, this version must ensure to perform a
// deep copy of the values...
return [].concat(...sources).map((element, index, array) => {
return callback(element, index, array, options);
});
}
catch (e) {
const reason = getErrorMessage(e);
throw new ArrayMergeError$1('Unable to merge arrays: ' + reason, {
cause: {
previous: e,
sources: sources
}
});
}
}
/**
* Resolves options
*
* @param {ArrayMergeCallback | ArrayMergeOptions} options
*
* @return {Readonly<ArrayMergeOptions>}
*
* @protected
*/
resolveOptions(options) {
return DefaultArrayMergeOptions.from(options);
}
}
/**
* Merge two or more arrays
*
* **Note**: _Method attempts to deep copy array values, via [structuredClone]{@link https://developer.mozilla.org/en-US/docs/Web/API/structuredClone}_
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/structuredClone
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/isConcatSpreadable
*
* @param {...any[]} sources
*
* @return {ArrayMerger|any[]}
*
* @throws {ArrayMergeError} If unable to merge arrays, e.g. if a value cannot be cloned via `structuredClone()`
*/
function merge(...sources /* eslint-disable-line @typescript-eslint/no-explicit-any */) {
const merger = new Merger();
if (sources.length == 0) {
return merger;
}
return merger.of(...sources);
}
/**
* Array Merge Error
*
* To be thrown when two or more arrays are unable to be merged.
*/
class ArrayMergeError extends Error {
/**
* Create a new Array Merge Error instance
*
* @param {string} [message]
* @param {ErrorOptions} [options]
*/
constructor(message, options) {
super(message, options);
configureCustomError(this);
}
}
export { ArrayMergeError, DefaultArrayMergeOptions, Merger, defaultArrayMergeCallback, includesAll, includesAny, isArrayLike, isConcatSpreadable, isSafeArrayLike, isTypedArray, merge };