« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/util/sugar.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/util/sugar.js')
-rw-r--r--src/util/sugar.js609
1 files changed, 320 insertions, 289 deletions
diff --git a/src/util/sugar.js b/src/util/sugar.js
index 99f706f..70672bf 100644
--- a/src/util/sugar.js
+++ b/src/util/sugar.js
@@ -6,69 +6,81 @@
 // 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';
+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;
+  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 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 filterEmptyLines = (string) =>
+  string
+    .split("\n")
+    .filter((line) => line.trim())
+    .join("\n");
 
-export const unique = arr => Array.from(new Set(arr));
+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)))));
+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 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) => {
+  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);
+          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()();
-    }
+  for (let i = 0; i < max && begin.length; i++) {
+    begin.shift()();
+  }
 
-    return ret;
+  return ret;
 }
 
 export function delay(ms) {
-    return new Promise(res => setTimeout(res, ms));
+  return new Promise((res) => setTimeout(res, ms));
 }
 
 // Stolen from here: https://stackoverflow.com/a/3561711
@@ -76,22 +88,22 @@ export function delay(ms) {
 // 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, '\\$&');
+  return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
 }
 
 export function bindOpts(fn, bind) {
-    const bindIndex = bind[bindOpts.bindIndex] ?? 1;
+  const bindIndex = bind[bindOpts.bindIndex] ?? 1;
 
-    const bound = function(...args) {
-        const opts = args[bindIndex] ?? {};
-        return fn(...args.slice(0, bindIndex), {...bind, ...opts});
-    };
+  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)`)
-    });
+  Object.defineProperty(bound, "name", {
+    value: fn.name ? `(options-bound) ${fn.name}` : `(options-bound)`,
+  });
 
-    return bound;
+  return bound;
 }
 
 bindOpts.bindIndex = Symbol();
@@ -108,103 +120,108 @@ bindOpts.bindIndex = Symbol();
 // 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
+  // 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));
+  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.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]);
+  aggregate.wrapAsync =
+    (fn) =>
+    (...args) => {
+      return fn(...args).then(
+        (value) => value,
+        (error) => {
+          errors.push(error);
+          return typeof returnOnFail === "function"
+            ? returnOnFail(...args)
+            : returnOnFail;
         }
+      );
     };
 
-    return aggregate;
+  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');
+openAggregate.errorClassSymbol = Symbol("error class");
 
 // Utility function for providing {errorClass} parameter to aggregate functions.
 export function aggregateThrows(errorClass) {
-    return {[openAggregate.errorClassSymbol]: errorClass};
+  return { [openAggregate.errorClassSymbol]: errorClass };
 }
 
 // Performs an ordinary array map with the given function, collating into a
@@ -217,36 +234,38 @@ export function aggregateThrows(errorClass) {
 // 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);
+  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);
+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
+  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 };
     });
-
-    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
@@ -257,162 +276,174 @@ export function _mapAggregate(mode, promiseAll, array, fn, aggregateOpts) {
 //
 // As with mapAggregate, the returned aggregate property is not yet closed.
 export function filterAggregate(array, fn, aggregateOpts) {
-    return _filterAggregate('sync', null, 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);
+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
+  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 };
     });
-
-    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);
+  return _withAggregate("sync", aggregateOpts, fn);
 }
 
 export function withAggregateAsync(aggregateOpts, fn) {
-    return _withAggregate('async', 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 (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;
+    });
+  }
+}
 
-    if (mode === 'sync') {
-        const result = fn(aggregate);
-        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 fn(aggregate).then(result => {
-            aggregate.close();
-            return result;
-        });
+      return header;
     }
-}
+  };
 
-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}));
+  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;
-        }
+  return (x, index, array) => {
+    try {
+      return fn(x, index, array);
+    } catch (error) {
+      error.message = `(${color.yellow(`#${index + 1}`)}) ${error.message}`;
+      throw error;
     }
+  };
 }