From f67f7152c0577e3a24f7e47fce0ecc97301713cb Mon Sep 17 00:00:00 2001 From: srgooglo Date: Mon, 6 Jun 2022 17:28:21 +0200 Subject: [PATCH] added utils --- .../server/src/utils/aggregate-error/index.js | 45 ++++ .../server/src/utils/clean-stack/index.js | 51 +++++ .../src/utils/escape-string-regexp/index.js | 12 ++ .../server/src/utils/indent-string/index.js | 39 ++++ packages/server/src/utils/pMap/index.js | 198 ++++++++++++++++++ 5 files changed, 345 insertions(+) create mode 100644 packages/server/src/utils/aggregate-error/index.js create mode 100644 packages/server/src/utils/clean-stack/index.js create mode 100644 packages/server/src/utils/escape-string-regexp/index.js create mode 100644 packages/server/src/utils/indent-string/index.js create mode 100644 packages/server/src/utils/pMap/index.js diff --git a/packages/server/src/utils/aggregate-error/index.js b/packages/server/src/utils/aggregate-error/index.js new file mode 100644 index 00000000..3ae0e5c2 --- /dev/null +++ b/packages/server/src/utils/aggregate-error/index.js @@ -0,0 +1,45 @@ +// Copyright (c) Sindre Sorhus (https://sindresorhus.com) +import indentString from '../indent-string'; +import cleanStack from '../clean-stack'; + +const cleanInternalStack = stack => stack.replace(/\s+at .*aggregate-error\/index.js:\d+:\d+\)?/g, ''); + +export default class AggregateError extends Error { + #errors; + + name = 'AggregateError'; + + constructor(errors) { + if (!Array.isArray(errors)) { + throw new TypeError(`Expected input to be an Array, got ${typeof errors}`); + } + + errors = errors.map(error => { + if (error instanceof Error) { + return error; + } + + if (error !== null && typeof error === 'object') { + // Handle plain error objects with message property and/or possibly other metadata + return Object.assign(new Error(error.message), error); + } + + return new Error(error); + }); + + let message = errors + .map(error => { + // The `stack` property is not standardized, so we can't assume it exists + return typeof error.stack === 'string' && error.stack.length > 0 ? cleanInternalStack(cleanStack(error.stack)) : String(error); + }) + .join('\n'); + message = '\n' + indentString(message, 4); + super(message); + + this.#errors = errors; + } + + get errors() { + return this.#errors.slice(); + } +} \ No newline at end of file diff --git a/packages/server/src/utils/clean-stack/index.js b/packages/server/src/utils/clean-stack/index.js new file mode 100644 index 00000000..01dfcfce --- /dev/null +++ b/packages/server/src/utils/clean-stack/index.js @@ -0,0 +1,51 @@ +// Copyright (c) Sindre Sorhus (https://sindresorhus.com) +import os from 'os'; +import escapeStringRegexp from '../escape-string-regexp'; + +const extractPathRegex = /\s+at.*[(\s](.*)\)?/; +const pathRegex = /^(?:(?:(?:node|node:[\w/]+|(?:(?:node:)?internal\/[\w/]*|.*node_modules\/(?:babel-polyfill|pirates)\/.*)?\w+)(?:\.js)?:\d+:\d+)|native)/; +const homeDir = typeof os.homedir === 'undefined' ? '' : os.homedir().replace(/\\/g, '/'); + +export default function cleanStack(stack, {pretty = false, basePath} = {}) { + const basePathRegex = basePath && new RegExp(`(at | \\()${escapeStringRegexp(basePath.replace(/\\/g, '/'))}`, 'g'); + + if (typeof stack !== 'string') { + return undefined; + } + + return stack.replace(/\\/g, '/') + .split('\n') + .filter(line => { + const pathMatches = line.match(extractPathRegex); + if (pathMatches === null || !pathMatches[1]) { + return true; + } + + const match = pathMatches[1]; + + // Electron + if ( + match.includes('.app/Contents/Resources/electron.asar') || + match.includes('.app/Contents/Resources/default_app.asar') || + match.includes('node_modules/electron/dist/resources/electron.asar') || + match.includes('node_modules/electron/dist/resources/default_app.asar') + ) { + return false; + } + + return !pathRegex.test(match); + }) + .filter(line => line.trim() !== '') + .map(line => { + if (basePathRegex) { + line = line.replace(basePathRegex, '$1'); + } + + if (pretty) { + line = line.replace(extractPathRegex, (m, p1) => m.replace(p1, p1.replace(homeDir, '~'))); + } + + return line; + }) + .join('\n'); +} \ No newline at end of file diff --git a/packages/server/src/utils/escape-string-regexp/index.js b/packages/server/src/utils/escape-string-regexp/index.js new file mode 100644 index 00000000..d49baa45 --- /dev/null +++ b/packages/server/src/utils/escape-string-regexp/index.js @@ -0,0 +1,12 @@ +// Copyright (c) Sindre Sorhus (https://sindresorhus.com) +export default function escapeStringRegexp(string) { + if (typeof string !== 'string') { + throw new TypeError('Expected a string'); + } + + // Escape characters with special meaning either inside or outside character sets. + // Use a simple backslash escape when it’s always valid, and a `\xnn` escape when the simpler form would be disallowed by Unicode patterns’ stricter grammar. + return string + .replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') + .replace(/-/g, '\\x2d'); +} \ No newline at end of file diff --git a/packages/server/src/utils/indent-string/index.js b/packages/server/src/utils/indent-string/index.js new file mode 100644 index 00000000..eafd265a --- /dev/null +++ b/packages/server/src/utils/indent-string/index.js @@ -0,0 +1,39 @@ +// Copyright (c) Sindre Sorhus (https://sindresorhus.com) +export default function indentString(string, count = 1, options = {}) { + const { + indent = ' ', + includeEmptyLines = false + } = options; + + if (typeof string !== 'string') { + throw new TypeError( + `Expected \`input\` to be a \`string\`, got \`${typeof string}\`` + ); + } + + if (typeof count !== 'number') { + throw new TypeError( + `Expected \`count\` to be a \`number\`, got \`${typeof count}\`` + ); + } + + if (count < 0) { + throw new RangeError( + `Expected \`count\` to be at least 0, got \`${count}\`` + ); + } + + if (typeof indent !== 'string') { + throw new TypeError( + `Expected \`options.indent\` to be a \`string\`, got \`${typeof indent}\`` + ); + } + + if (count === 0) { + return string; + } + + const regex = includeEmptyLines ? /^/gm : /^(?!\s*$)/gm; + + return string.replace(regex, indent.repeat(count)); +} \ No newline at end of file diff --git a/packages/server/src/utils/pMap/index.js b/packages/server/src/utils/pMap/index.js new file mode 100644 index 00000000..7a47c36c --- /dev/null +++ b/packages/server/src/utils/pMap/index.js @@ -0,0 +1,198 @@ +// Copyright (c) Sindre Sorhus (https://sindresorhus.com) +import AggregateError from "../aggregate-error"; + +/** +An error to be thrown when the request is aborted by AbortController. +DOMException is thrown instead of this Error when DOMException is available. +*/ +export class AbortError extends Error { + constructor(message) { + super(); + this.name = "AbortError"; + this.message = message; + } +} + +/** +TODO: Remove AbortError and just throw DOMException when targeting Node 18. +*/ +const getDOMException = errorMessage => globalThis.DOMException === undefined + ? new AbortError(errorMessage) + : new DOMException(errorMessage); + +/** +TODO: Remove below function and just "reject(signal.reason)" when targeting Node 18. +*/ +const getAbortedReason = signal => { + const reason = signal.reason === undefined + ? getDOMException("This operation was aborted.") + : signal.reason; + + return reason instanceof Error ? reason : getDOMException(reason); +}; + +export default async function pMap( + iterable, + mapper, + { + concurrency = Number.POSITIVE_INFINITY, + stopOnError = true, + signal, + } = {}, +) { + return new Promise((resolve, reject_) => { + if (iterable[Symbol.iterator] === undefined && iterable[Symbol.asyncIterator] === undefined) { + throw new TypeError(`Expected \`input\` to be either an \`Iterable\` or \`AsyncIterable\`, got (${typeof iterable})`); + } + + if (typeof mapper !== "function") { + throw new TypeError("Mapper function is required"); + } + + if (!((Number.isSafeInteger(concurrency) || concurrency === Number.POSITIVE_INFINITY) && concurrency >= 1)) { + throw new TypeError(`Expected \`concurrency\` to be an integer from 1 and up or \`Infinity\`, got \`${concurrency}\` (${typeof concurrency})`); + } + + const result = []; + const errors = []; + const skippedIndexesMap = new Map(); + let isRejected = false; + let isResolved = false; + let isIterableDone = false; + let resolvingCount = 0; + let currentIndex = 0; + const iterator = iterable[Symbol.iterator] === undefined ? iterable[Symbol.asyncIterator]() : iterable[Symbol.iterator](); + + const reject = reason => { + isRejected = true; + isResolved = true; + reject_(reason); + }; + + if (signal) { + if (signal.aborted) { + reject(getAbortedReason(signal)); + } + + signal.addEventListener("abort", () => { + reject(getAbortedReason(signal)); + }); + } + + const next = async () => { + if (isResolved) { + return; + } + + const nextItem = await iterator.next(); + + const index = currentIndex; + currentIndex++; + + // Note: `iterator.next()` can be called many times in parallel. + // This can cause multiple calls to this `next()` function to + // receive a `nextItem` with `done === true`. + // The shutdown logic that rejects/resolves must be protected + // so it runs only one time as the `skippedIndex` logic is + // non-idempotent. + if (nextItem.done) { + isIterableDone = true; + + if (resolvingCount === 0 && !isResolved) { + if (!stopOnError && errors.length > 0) { + reject(new AggregateError(errors)); + return; + } + + isResolved = true; + + if (skippedIndexesMap.size === 0) { + resolve(result); + return; + } + + const pureResult = []; + + // Support multiple `pMapSkip`"s. + for (const [index, value] of result.entries()) { + if (skippedIndexesMap.get(index) === pMapSkip) { + continue; + } + + pureResult.push(value); + } + + resolve(pureResult); + } + + return; + } + + resolvingCount++; + + // Intentionally detached + (async () => { + try { + const element = await nextItem.value; + + if (isResolved) { + return; + } + + const value = await mapper(element, index); + + // Use Map to stage the index of the element. + if (value === pMapSkip) { + skippedIndexesMap.set(index, value); + } + + result[index] = value; + + resolvingCount--; + await next(); + } catch (error) { + if (stopOnError) { + reject(error); + } else { + errors.push(error); + resolvingCount--; + + // In that case we can"t really continue regardless of `stopOnError` state + // since an iterable is likely to continue throwing after it throws once. + // If we continue calling `next()` indefinitely we will likely end up + // in an infinite loop of failed iteration. + try { + await next(); + } catch (error) { + reject(error); + } + } + } + })(); + }; + + // Create the concurrent runners in a detached (non-awaited) + // promise. We need this so we can await the `next()` calls + // to stop creating runners before hitting the concurrency limit + // if the iterable has already been marked as done. + // NOTE: We *must* do this for async iterators otherwise we"ll spin up + // infinite `next()` calls by default and never start the event loop. + (async () => { + for (let index = 0; index < concurrency; index++) { + try { + // eslint-disable-next-line no-await-in-loop + await next(); + } catch (error) { + reject(error); + break; + } + + if (isIterableDone || isRejected) { + break; + } + } + })(); + }); +} + +export const pMapSkip = Symbol("skip"); \ No newline at end of file