lib/general.js
/**
* Retrieves the property of an object, or item in array at index, based
* on the supplied path.
* @example
* const obj = { a: { b: { c: [1, 2, 3] } } }
* const two = getViaPath(obj, 'a', 'b', 'c', 1); // two == 2
* @param {*} obj - An object
* @param {*} pathItems
* @return {*}
*/
export function getViaPath(obj, ...pathItems) {
if (obj == null || pathItems.length === 0) return null;
let cur = obj[pathItems[0]];
for (let i = 1; i < pathItems.length; i++) {
cur = cur[pathItems[i]];
if (cur == null) return null;
}
return cur;
}
/**
* Initiates an pywb/webrecorder auto-fetch of content
*/
export function autoFetchFromDoc() {
if (window.$WBAutoFetchWorker$) {
window.$WBAutoFetchWorker$.extractFromLocalDoc();
}
}
/**
* Sends the supplied array of URLs to the backing pywb/webrecorder auto-fetch worker if it exists
* @param {Array<string>} urls
*/
export function sendAutoFetchWorkerURLs(urls) {
if (window.$WBAutoFetchWorker$) {
window.$WBAutoFetchWorker$.justFetch(urls);
}
}
/**
* This function is a no op
*/
export function noop() {}
/**
* Automatically binds the non-inherited functions of the supplied
* class to itself.
* @param clazz
*/
export function autobind(clazz) {
const clazzProps = Object.getOwnPropertyNames(clazz.constructor.prototype);
let prop;
let propValue;
for (var i = 0; i < clazzProps.length; ++i) {
prop = clazzProps[i];
propValue = clazz[prop];
if (prop !== 'constructor' && typeof propValue === 'function') {
clazz[prop] = propValue.bind(clazz);
}
}
}
/**
* Returns T/F if the supplied object has all of the supplied properties.
* The existence check is `obj[prop] != null`
* @param {Object} obj - The object to be tested
* @param {...string} props - The property names
* @return {boolean}
*/
export function objectHasProps(obj, ...props) {
if (obj == null) return false;
for (var i = 0; i < props.length; ++i) {
if (obj[props[i]] == null) return false;
}
return true;
}
/**
* Returns T/F if an global property (on window) exists and has
* all properties. The existence check is `obj[prop] != null`
* @param {string} global - The name of the global
* @param {...string} props - The property names
* @return {boolean}
*/
export function globalWithPropsExist(global, ...props) {
const obj = window[global];
if (obj == null) return false;
for (var i = 0; i < props.length; ++i) {
if (obj[props[i]] == null) return false;
}
return true;
}
/**
* Returns a new object with the properties of the object
* on the new object
* @param {?Object} object - The object to extract properties from
* @param {...string} props - The property names to be extracted
* @return {?Object} - The new object if the original object was not null
*/
export function extractProps(object, ...props) {
if (object == null) return null;
const extracted = {};
for (var i = 0; i < props.length; ++i) {
extracted[props[i]] = object[props[i]];
}
return extracted;
}
/**
* Composes multiple functions from right to left into a single function.
* The rightmost function can take multiple arguments with the remaining
* functions taking only a single argument, the return value of the previous
* invocation.
* @param {...function(...args: *): *} funcs - The functions to compose.
* @return {function(...args: *): *} - A function obtained by composing the functions
* from right to left. For example, compose(f, g, h) is identical to doing
* (...args) => f(g(h(...args))).
*/
export function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg;
}
if (funcs.length === 1) {
return funcs[0];
}
return funcs.reduce((a, b) => (...args) => a(b(...args)));
}
/**
* Composes multiple async or Promise returning functions from right to
* left into a single function. The rightmost function can take multiple arguments
* with the remaining functions taking only a single argument, the return value
* of the previous invocation.
* @param {...function(...args: *): Promise<*>} funcs - The functions to compose.
* @return {function(...args: *): Promise<*>} - A function obtained by composing the functions
* from right to left. For example, composeAsync(f, g, h) is identical to doing
* (...args) => h(...args).then(result => g(result).then(result => f(result)).
*/
export function composeAsync(...funcs) {
if (funcs.length === 0) {
return async function() {
return arguments[0];
};
}
if (funcs.length === 1) {
return funcs[0];
}
return funcs.reduce((a, b) => (...args) => b(...args).then(a));
}
/**
* Composes multiple Iterator returning functions from right to
* left into a single function. The rightmost function can take multiple arguments
* with the remaining functions taking only a single argument, the iterator returned
* by the previous invocation. The composition of iterators can thought of
* as chaining multiple iterators together, that is to say feeding the yielded results
* of each iterator from right to left. This can be useful for chaining cascading
* ordered actions.
* @example
* function* iter1(iter) {
* console.log('start iter1');
* for (const value of iter) {
* yield `iter1 got "${value}" from the previous iter`;
* }
* yield 'end iter1';
* }
*
* function* iter2(iter) {
* console.log('start iter2');
* for (const value of iter) {
* yield `iter2 got "${value}" from the previous iter`;
* }
* yield 'end iter2';
* }
*
* function* iter3(...args) {
* console.log('start iter3');
* let i = args.length;
* while (i--) {
* yield args[i];
* }
* yield 'end iter3';
* }
*
* const finalIter = composeIterators(iter1, iter2, iter3)(1, 2, 3, 4, 5);
*
* for (const it of finalIter) {
* console.log(it);
* }
*
* // The output is as follows
* // logs "start iter" from iter1
* // logs "start iter2" from iter2
* // logs "start iter3" from iter3
* // logs 'iter1 got "iter2 got "5" from the previous iter" from the previous iter' from the for of finalIter
* // logs 'iter1 got "iter2 got "4" from the previous iter" from the previous iter' from the for of finalIter
* // logs 'iter1 got "iter2 got "3" from the previous iter" from the previous iter' from the for of finalIter
* // logs 'iter1 got "iter2 got "2" from the previous iter" from the previous iter' from the for of finalIter
* // logs 'iter1 got "iter2 got "1" from the previous iter" from the previous iter' from the for of finalIter
* // logs 'iter1 got "iter2 got "end iter3" from the previous iter" from the previous iter' from the for of finalIter
* // logs 'iter1 got "end iter2" from the previous iter' from the for of finalIter
* // logs 'end iter1' from the for of finalIter
*
* @param {...function(...args:*): Iterator<*>} funcs - The Iterator returning functions to be composed (chained)
* @return {function(...args:*): Iterator<*>} - A function obtained by composing the functions
* from right to left. See the documentations example for details
*/
export function composeIterators(...funcs) {
if (funcs.length === 0) {
return function*() {
yield arguments[0];
};
}
if (funcs.length === 1) {
return funcs[0];
}
return funcs.reduce(
(a, b) =>
function*(...args) {
yield* a(b(...args));
}
);
}
/**
* Composes multiple AsyncIterator returning functions from right to
* left into a single function. The rightmost function can take multiple arguments
* with the remaining functions taking only a single argument, the iterator returned
* by the previous invocation. The composition of AsyncIterator can thought of
* as chaining multiple AsyncIterator together, that is to say feeding the yielded results
* of each iterator from right to left. This can be useful for chaining cascading asynchronous
* ordered actions.
* @example
* async function* iter1(iter) {
* console.log('start iter1');
* for await (const value of iter) {
* yield `iter1 got "${value}" from the previous iter`;
* }
* yield 'end iter1';
* }
*
* async function* iter2(iter) {
* console.log('start iter2');
* for await (const value of iter) {
* yield `iter2 got "${value}" from the previous iter`;
* }
* yield 'end iter2';
* }
*
* async function* iter3(...args) {
* console.log('start iter3');
* let i = args.length;
* while (i--) {
* yield args[i];
* }
* yield 'end iter3';
* }
*
* const finalAIter = composeAsyncIterators(iter1, iter2, iter3)(1, 2, 3, 4, 5);
*
* (async () => {
* for await (const it of finalAIter) {
* console.log(it);
* }
* })();
*
* // The output is as follows
* // logs "start iter" from iter1
* // logs "start iter2" from iter2
* // logs "start iter3" from iter3
* // logs 'iter1 got "iter2 got "5" from the previous iter" from the previous iter' from the for await of finalAiter
* // logs 'iter1 got "iter2 got "4" from the previous iter" from the previous iter' from the for await of finalAiter
* // logs 'iter1 got "iter2 got "3" from the previous iter" from the previous iter' from the for await of finalAiter
* // logs 'iter1 got "iter2 got "2" from the previous iter" from the previous iter' from the for await of finalAiter
* // logs 'iter1 got "iter2 got "1" from the previous iter" from the previous iter' from the for await of finalAiter
* // logs 'iter1 got "iter2 got "end iter3" from the previous iter" from the previous iter' from the for await of finalAiter
* // logs 'iter1 got "end iter2" from the previous iter' from the for await of finalAiter
* // logs 'end iter1' from the for await of finalAiter
*
* @param {...function(...args:*): AsyncIterator<*>} funcs - The AsyncIterator returning functions to be composed (chained)
* @return {function(...args:*): AsyncIterator<*>} - A function obtained by composing the functions
* from right to left. See the documentations example for details
*/
export function composeAsyncIterators(...funcs) {
if (funcs.length === 0) {
return async function*() {
yield arguments[0];
};
}
if (funcs.length === 1) {
return funcs[0];
}
return funcs.reduce(
(a, b) =>
async function*(...args) {
yield* a(b(...args));
}
);
}
/**
* Creates a Promise that is resolvable externally returning an object
* that exposes the promise itself and the resolve and reject functions
* passed as arguments to the executor function
* @return {{resolve: function, reject: function, promise: Promise<*>}}
*/
export function promiseResolveReject() {
const promResolveReject = { promise: null, resolve: null, reject: null };
promResolveReject.promise = new Promise((resolve, reject) => {
promResolveReject.resolve = resolve;
promResolveReject.reject = reject;
});
return promResolveReject;
}
/**
* Returns T/F indicating if the supplied object is an instance of some class.
* If the object to be tested is falsy false is returned otherwise the results
* of the instanceof check.
* @param {Object} obj - The object to be tested
* @param {Object} shouldBeInstanceOfThis - The class the obj should be an instance of
* @return {boolean}
*/
export function objectInstanceOf(obj, shouldBeInstanceOfThis) {
if (!obj) return false;
return obj instanceof shouldBeInstanceOfThis;
}
/**
* Creates and returns a partially applied function with args being applied left to right
* @param {function} fn - The function to partially apply arguments to
* @param {*} [a1] - 1st arg
* @param {*} [a2] - 2nd arg
* @param {*} [a3] - 3rd arg
* @param {*} [a4] - 4th arg
* @param {*} [a5] - 5th arg
* @param {...*} [aN] - The remaining arguments to be partially applied
* @return {function(...args: *): *} - Returns the new partially applied function
*/
export function partial(fn, a1, a2, a3, a4, a5, ...aN) {
if (a1 == null) return fn;
if (aN.length) {
return (...args) => fn(a1, a2, a3, a4, a5, ...aN, ...args);
}
if (a5 != null) return (...args) => fn(a1, a2, a3, a4, a5, ...args);
if (a4 != null) return (...args) => fn(a1, a2, a3, a4, ...args);
if (a3 != null) return (...args) => fn(a1, a2, a3, ...args);
if (a2 != null) return (...args) => fn(a1, a2, ...args);
return (...args) => fn(a1, ...args);
}
/**
* Creates and returns a partially applied function with args being applied right to left
* @param {function} fn - The function to partially apply arguments to
* @param {*} [a1] - 1st nth arg
* @param {*} [a2] - 2nd nth arg
* @param {*} [a3] - 3rd nth arg
* @param {*} [a4] - 4th nth arg
* @param {*} [a5] - 5th nth arg
* @param {...*} [aN] - The remaining arguments to be partially applied
* @return {function(...args: *): *} - Returns the new partially applied function
*/
export function partialRight(fn, a1, a2, a3, a4, a5, ...aN) {
if (a1 == null) return fn;
if (aN.length) {
return (...args) => fn(...args, a1, a2, a3, a4, a5, ...aN);
}
if (a5 != null) return (...args) => fn(...args, a1, a2, a3, a4, a5);
if (a4 != null) return (...args) => fn(...args, a1, a2, a3, a4);
if (a3 != null) return (...args) => fn(...args, a1, a2, a3);
if (a2 != null) return (...args) => fn(...args, a1, a2);
return (...args) => fn(...args, a1);
}
/** @ignore */
let __BytesToHex__;
/**
* Creates and returns a valid uuid v4
* @return {string}
*/
export function uuidv4() {
if (__BytesToHex__ == null) {
__BytesToHex__ = new Array(256);
for (let i = 0; i < 256; ++i) {
__BytesToHex__[i] = (i + 0x100).toString(16).substr(1);
}
}
const randomBytes = crypto.getRandomValues(new Uint8Array(16));
randomBytes[6] = (randomBytes[6] & 0x0f) | 0x40;
randomBytes[8] = (randomBytes[8] & 0x3f) | 0x80;
return [
__BytesToHex__[randomBytes[0]],
__BytesToHex__[randomBytes[1]],
__BytesToHex__[randomBytes[2]],
__BytesToHex__[randomBytes[3]],
'-',
__BytesToHex__[randomBytes[4]],
__BytesToHex__[randomBytes[5]],
'-',
__BytesToHex__[randomBytes[6]],
__BytesToHex__[randomBytes[7]],
'-',
__BytesToHex__[randomBytes[8]],
__BytesToHex__[randomBytes[9]],
'-',
__BytesToHex__[randomBytes[10]],
__BytesToHex__[randomBytes[11]],
__BytesToHex__[randomBytes[12]],
__BytesToHex__[randomBytes[13]],
__BytesToHex__[randomBytes[14]],
__BytesToHex__[randomBytes[15]],
].join('');
}
/**
* Returns T/F indicating if the supplied object is a generator or async generator
* @param {*} obj
* @return {boolean}
*/
export function isGenerator(obj) {
if (!obj) return false;
const tag = obj[Symbol.toStringTag];
if (tag === 'AsyncGenerator' || tag === 'Generator') return true;
if (isFunction(obj.next) && isFunction(obj.throw) && isFunction(obj.return)) {
return true;
}
if (!obj.constructor) return false;
const ctag = obj.constructor[Symbol.toStringTag];
return ctag === 'GeneratorFunction' || ctag === 'AsyncGeneratorFunction';
}
/**
* Returns T/F indicating if the supplied object is a Promise or Promise like
* @param {*} obj
* @return {boolean}
*/
export function isPromise(obj) {
if (!obj) return false;
return (
obj instanceof Promise ||
(typeof obj === 'object' &&
isFunction(obj.then) &&
isFunction(obj.catch)) ||
obj[Symbol.toStringTag] === 'Promise'
);
}
/**
* Returns T/F indicating if the supplied arument is a function
* @param {*} obj
* @return {boolean}
*/
export function isFunction(obj) {
return typeof obj === 'function';
}
/**
* Like map for arrays but over async-iterators
* @param {AsyncIterableIterator<*>} iterator - The iterator
* to have a mapping function applied over
* @param {function(arg: *): *} mapper - The mapping function
* @return {AsyncIterableIterator<*>}
*/
export async function* mapAsyncIterator(iterator, mapper) {
for await (const item of iterator) {
const nextValue = mapper(item);
if (isGenerator(nextValue)) {
for await (const next of noExceptGeneratorWrap(nextValue)) {
yield next;
}
} else yield nextValue;
}
}
/**
* Wraps the supplied generator in a try catch and re-yields its values generator.
* If the wrapped generator throws an exception the wrapping generator ends.
* @param {AsyncIterableIterator<*>|IterableIterator<*>} generator - The generator to be wrapped
* @param {boolean} [returnLast] - Should the last value of the supplied generator be returned
* @return {AsyncIterableIterator<*>}
*/
export async function* noExceptGeneratorWrap(generator, returnLast) {
try {
let next;
let nv;
while (true) {
next = generator.next();
if (isPromise(next)) nv = await next;
else nv = next;
if (nv.done) {
if (nv.value) {
if (returnLast) return nv.value;
else yield nv.value;
}
break;
} else {
yield nv.value;
}
}
} catch (e) {}
}