diff options
Diffstat (limited to 'src/util/sugar.js')
-rw-r--r-- | src/util/sugar.js | 418 |
1 files changed, 0 insertions, 418 deletions
diff --git a/src/util/sugar.js b/src/util/sugar.js deleted file mode 100644 index 99f706f1..00000000 --- a/src/util/sugar.js +++ /dev/null @@ -1,418 +0,0 @@ -// Syntactic sugar! (Mostly.) -// Generic functions - these are useful just a8out everywhere. -// -// Friendly(!) disclaimer: these utility functions haven't 8een tested all that -// much. Do not assume it will do exactly what you want it to do in all cases. -// It will likely only do exactly what I want it to, and only in the cases I -// decided were relevant enough to 8other handling. - -import { color } from './cli.js'; - -// Apparently JavaScript doesn't come with a function to split an array into -// chunks! Weird. Anyway, this is an awesome place to use a generator, even -// though we don't really make use of the 8enefits of generators any time we -// actually use this. 8ut it's still awesome, 8ecause I say so. -export function* splitArray(array, fn) { - let lastIndex = 0; - while (lastIndex < array.length) { - let nextIndex = array.findIndex((item, index) => index >= lastIndex && fn(item)); - if (nextIndex === -1) { - nextIndex = array.length; - } - yield array.slice(lastIndex, nextIndex); - // Plus one because we don't want to include the dividing line in the - // next array we yield. - lastIndex = nextIndex + 1; - } -}; - -export const mapInPlace = (array, fn) => array.splice(0, array.length, ...array.map(fn)); - -export const filterEmptyLines = string => string.split('\n').filter(line => line.trim()).join('\n'); - -export const unique = arr => Array.from(new Set(arr)); - -export const compareArrays = (arr1, arr2, {checkOrder = true} = {}) => ( - arr1.length === arr2.length && (checkOrder - ? (arr1.every((x, i) => arr2[i] === x)) - : (arr1.every(x => arr2.includes(x))))); - -// Stolen from jq! Which pro8a8ly stole the concept from other places. Nice. -export const withEntries = (obj, fn) => Object.fromEntries(fn(Object.entries(obj))); - -export function queue(array, max = 50) { - if (max === 0) { - return array.map(fn => fn()); - } - - const begin = []; - let current = 0; - const ret = array.map(fn => new Promise((resolve, reject) => { - begin.push(() => { - current++; - Promise.resolve(fn()).then(value => { - current--; - if (current < max && begin.length) { - begin.shift()(); - } - resolve(value); - }, reject); - }); - })); - - for (let i = 0; i < max && begin.length; i++) { - begin.shift()(); - } - - return ret; -} - -export function delay(ms) { - return new Promise(res => setTimeout(res, ms)); -} - -// Stolen from here: https://stackoverflow.com/a/3561711 -// -// There's a proposal for a native JS function like this, 8ut it's not even -// past stage 1 yet: https://github.com/tc39/proposal-regex-escaping -export function escapeRegex(string) { - return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); -} - -export function bindOpts(fn, bind) { - const bindIndex = bind[bindOpts.bindIndex] ?? 1; - - const bound = function(...args) { - const opts = args[bindIndex] ?? {}; - return fn(...args.slice(0, bindIndex), {...bind, ...opts}); - }; - - Object.defineProperty(bound, 'name', { - value: (fn.name ? `(options-bound) ${fn.name}` : `(options-bound)`) - }); - - return bound; -} - -bindOpts.bindIndex = Symbol(); - -// Utility function for providing useful interfaces to the JS AggregateError -// class. -// -// Generally, this works by returning a set of interfaces which operate on -// functions: wrap() takes a function and returns a new function which passes -// its arguments through and appends any resulting error to the internal error -// list; call() simplifies this process by wrapping the provided function and -// then calling it immediately. Once the process for which errors should be -// aggregated is complete, close() constructs and throws an AggregateError -// object containing all caught errors (or doesn't throw anything if there were -// no errors). -export function openAggregate({ - // Constructor to use, defaulting to the builtin AggregateError class. - // Anything passed here should probably extend from that! May be used for - // letting callers programatically distinguish between multiple aggregate - // errors. - // - // This should be provided using the aggregateThrows utility function. - [openAggregate.errorClassSymbol]: errorClass = AggregateError, - - // Optional human-readable message to describe the aggregate error, if - // constructed. - message = '', - - // Value to return when a provided function throws an error. If this is a - // function, it will be called with the arguments given to the function. - // (This is primarily useful when wrapping a function and then providing it - // to another utility, e.g. array.map().) - returnOnFail = null -} = {}) { - const errors = []; - - const aggregate = {}; - - aggregate.wrap = fn => (...args) => { - try { - return fn(...args); - } catch (error) { - errors.push(error); - return (typeof returnOnFail === 'function' - ? returnOnFail(...args) - : returnOnFail); - } - }; - - aggregate.wrapAsync = fn => (...args) => { - return fn(...args).then( - value => value, - error => { - errors.push(error); - return (typeof returnOnFail === 'function' - ? returnOnFail(...args) - : returnOnFail); - }); - }; - - aggregate.call = (fn, ...args) => { - return aggregate.wrap(fn)(...args); - }; - - aggregate.callAsync = (fn, ...args) => { - return aggregate.wrapAsync(fn)(...args); - }; - - aggregate.nest = (...args) => { - return aggregate.call(() => withAggregate(...args)); - }; - - aggregate.nestAsync = (...args) => { - return aggregate.callAsync(() => withAggregateAsync(...args)); - }; - - aggregate.map = (...args) => { - const parent = aggregate; - const { result, aggregate: child } = mapAggregate(...args); - parent.call(child.close); - return result; - }; - - aggregate.mapAsync = async (...args) => { - const parent = aggregate; - const { result, aggregate: child } = await mapAggregateAsync(...args); - parent.call(child.close); - return result; - }; - - aggregate.filter = (...args) => { - const parent = aggregate; - const { result, aggregate: child } = filterAggregate(...args); - parent.call(child.close); - return result; - }; - - aggregate.throws = aggregateThrows; - - aggregate.close = () => { - if (errors.length) { - throw Reflect.construct(errorClass, [errors, message]); - } - }; - - return aggregate; -} - -openAggregate.errorClassSymbol = Symbol('error class'); - -// Utility function for providing {errorClass} parameter to aggregate functions. -export function aggregateThrows(errorClass) { - return {[openAggregate.errorClassSymbol]: errorClass}; -} - -// Performs an ordinary array map with the given function, collating into a -// results array (with errored inputs filtered out) and an error aggregate. -// -// Optionally, override returnOnFail to disable filtering and map errored inputs -// to a particular output. -// -// Note the aggregate property is the result of openAggregate(), still unclosed; -// use aggregate.close() to throw the error. (This aggregate may be passed to a -// parent aggregate: `parent.call(aggregate.close)`!) -export function mapAggregate(array, fn, aggregateOpts) { - return _mapAggregate('sync', null, array, fn, aggregateOpts); -} - -export function mapAggregateAsync(array, fn, { - promiseAll = Promise.all.bind(Promise), - ...aggregateOpts -} = {}) { - return _mapAggregate('async', promiseAll, array, fn, aggregateOpts); -} - -// Helper function for mapAggregate which holds code common between sync and -// async versions. -export function _mapAggregate(mode, promiseAll, array, fn, aggregateOpts) { - const failureSymbol = Symbol(); - - const aggregate = openAggregate({ - returnOnFail: failureSymbol, - ...aggregateOpts - }); - - if (mode === 'sync') { - const result = array.map(aggregate.wrap(fn)) - .filter(value => value !== failureSymbol); - return {result, aggregate}; - } else { - return promiseAll(array.map(aggregate.wrapAsync(fn))).then(values => { - const result = values.filter(value => value !== failureSymbol); - return {result, aggregate}; - }); - } -} - -// Performs an ordinary array filter with the given function, collating into a -// results array (with errored inputs filtered out) and an error aggregate. -// -// Optionally, override returnOnFail to disable filtering errors and map errored -// inputs to a particular output. -// -// As with mapAggregate, the returned aggregate property is not yet closed. -export function filterAggregate(array, fn, aggregateOpts) { - return _filterAggregate('sync', null, array, fn, aggregateOpts); -} - -export async function filterAggregateAsync(array, fn, { - promiseAll = Promise.all.bind(Promise), - ...aggregateOpts -} = {}) { - return _filterAggregate('async', promiseAll, array, fn, aggregateOpts); -} - -// Helper function for filterAggregate which holds code common between sync and -// async versions. -function _filterAggregate(mode, promiseAll, array, fn, aggregateOpts) { - const failureSymbol = Symbol(); - - const aggregate = openAggregate({ - returnOnFail: failureSymbol, - ...aggregateOpts - }); - - function filterFunction(value) { - // Filter out results which match the failureSymbol, i.e. errored - // inputs. - if (value === failureSymbol) return false; - - // Always keep results which match the overridden returnOnFail - // value, if provided. - if (value === aggregateOpts.returnOnFail) return true; - - // Otherwise, filter according to the returned value of the wrapped - // function. - return value.output; - } - - function mapFunction(value) { - // Then turn the results back into their corresponding input, or, if - // provided, the overridden returnOnFail value. - return (value === aggregateOpts.returnOnFail - ? value - : value.input); - } - - function wrapperFunction(x, ...rest) { - return { - input: x, - output: fn(x, ...rest) - }; - } - - if (mode === 'sync') { - const result = array - .map(aggregate.wrap((input, index, array) => { - const output = fn(input, index, array); - return {input, output}; - })) - .filter(filterFunction) - .map(mapFunction); - - return {result, aggregate}; - } else { - return promiseAll(array.map(aggregate.wrapAsync(async (input, index, array) => { - const output = await fn(input, index, array); - return {input, output}; - }))).then(values => { - const result = values - .filter(filterFunction) - .map(mapFunction); - - return {result, aggregate}; - }); - } -} - -// Totally sugar function for opening an aggregate, running the provided -// function with it, then closing the function and returning the result (if -// there's no throw). -export function withAggregate(aggregateOpts, fn) { - return _withAggregate('sync', aggregateOpts, fn); -} - -export function withAggregateAsync(aggregateOpts, fn) { - return _withAggregate('async', aggregateOpts, fn); -} - -export function _withAggregate(mode, aggregateOpts, fn) { - if (typeof aggregateOpts === 'function') { - fn = aggregateOpts; - aggregateOpts = {}; - } - - const aggregate = openAggregate(aggregateOpts); - - if (mode === 'sync') { - const result = fn(aggregate); - aggregate.close(); - return result; - } else { - return fn(aggregate).then(result => { - aggregate.close(); - return result; - }); - } -} - -export function showAggregate(topError, { - pathToFile = p => p, - showTraces = true -} = {}) { - const recursive = (error, {level}) => { - let header = (showTraces - ? `[${error.constructor.name || 'unnamed'}] ${error.message || '(no message)'}` - : (error instanceof AggregateError - ? `[${error.message || '(no message)'}]` - : error.message || '(no message)')); - if (showTraces) { - const stackLines = error.stack?.split('\n'); - const stackLine = stackLines?.find(line => - line.trim().startsWith('at') - && !line.includes('sugar') - && !line.includes('node:') - && !line.includes('<anonymous>')); - const tracePart = (stackLine - ? '- ' + stackLine.trim().replace(/file:\/\/(.*\.js)/, (match, pathname) => pathToFile(pathname)) - : '(no stack trace)'); - header += ` ${color.dim(tracePart)}`; - } - const bar = (level % 2 === 0 - ? '\u2502' - : color.dim('\u254e')); - const head = (level % 2 === 0 - ? '\u257f' - : color.dim('\u257f')); - - if (error instanceof AggregateError) { - return header + '\n' + (error.errors - .map(error => recursive(error, {level: level + 1})) - .flatMap(str => str.split('\n')) - .map((line, i, lines) => (i === 0 - ? ` ${head} ${line}` - : ` ${bar} ${line}`)) - .join('\n')); - } else { - return header; - } - }; - - console.error(recursive(topError, {level: 0})); -} - -export function decorateErrorWithIndex(fn) { - return (x, index, array) => { - try { - return fn(x, index, array); - } catch (error) { - error.message = `(${color.yellow(`#${index + 1}`)}) ${error.message}`; - throw error; - } - } -} |