UNPKG

@clickup/ent-framework

Version:

A PostgreSQL graph-database-alike library with microsharding and row-level security

297 lines 10.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.join = join; exports.mapJoin = mapJoin; exports.jitter = jitter; exports.copyStack = copyStack; exports.minifyStack = minifyStack; exports.localUniqueInt = localUniqueInt; exports.stringHash = stringHash; exports.objectHash = objectHash; exports.jsonHash = jsonHash; exports.indent = indent; exports.addSentenceSuffixes = addSentenceSuffixes; exports.firstLine = firstLine; exports.inspectCompact = inspectCompact; exports.sanitizeIDForDebugPrinting = sanitizeIDForDebugPrinting; exports.nullthrows = nullthrows; exports.runInVoid = runInVoid; exports.hasKey = hasKey; exports.entries = entries; exports.maybeCall = maybeCall; exports.maybeAsyncCall = maybeAsyncCall; const crypto_1 = require("crypto"); const util_1 = require("util"); const compact_1 = __importDefault(require("lodash/compact")); const object_hash_1 = __importDefault(require("object-hash")); /** * A safe replacement for Promise-all built-in method. * * Works the same way as Promise-all, but additionally guarantees that ALL OTHER * promises have settled in case one of them rejects. This is needed to ensure * that we never have unexpected "dangling" promises continuing running in * nowhere in case one of the promises rejects early (the behavior of * Promise.all is to reject eagerly and let the rest of stuff running whilst the * caller code unfreezes). * * The behavior of join() is similar to Promise.allSettled(), but it throws the * 1st exception occurred; this is what's expected in most of the cases, and * this is how promises are implemented in e.g. Hack. * * The benefits of ensuring everything is settled: * * 1. We never have surprising entries in our logs (e.g. imagine a request * aborted long time ago, and then some "dangling" promises continue running * and issue queries as if nothing happened). * 2. Predictable control flow: if we run `await join()`, we know that no side * effects from the spawned promises will appear after this await throws or * returns. * * "Join" is a term from parallel programming (e.g. "join threads"), it’s pretty * concrete and means that after the call, multiple parallel execution flows * “join” into one. It's a word to describe having "one" from "many". * * What’s interesting is that, besides Promise-all leaks execution flows, it * still doesn’t trigger unhandledRejection for them in case one of them throws * later, it just swallows all other exceptions. * * I.e. Promise-all means "run all in parallel, if one throws - throw * immediately and let the others continue running in nowhere; if some of THAT * others throws, swallow their exceptions". * * And join() means "run all in parallel, if one throws - wait until everyone * finishes, and then throw the 1st exception; if some of others throw, swallow * their exceptions". * * See also https://en.wikipedia.org/wiki/Fork%E2%80%93join_model */ async function join(promises) { const promisesArray = promises instanceof Array ? promises : Object.values(promises); let firstError = undefined; let errorCount = 0; const resultsArray = await Promise["all"](promisesArray.map(async (promise) => Promise.resolve(promise).catch((err) => { if (errorCount === 0) { firstError = err; } errorCount++; return undefined; }))); if (errorCount > 0) { throw firstError; } return promises instanceof Array ? resultsArray : Object.fromEntries(Object.keys(promises).map((key, i) => [key, resultsArray[i]])); } /** * A shortcut for `await join(arr.map(async ...))`. */ async function mapJoin(arr, func) { return join((await arr).map((e, idx) => func(e, idx))); } /** * Returns a random value between 1 and 1+jitter. */ function jitter(jitter) { return 1 + jitter * Math.random(); } /** * Copies a stack-trace from fromErr error into toErr object. Useful for * lightweight exceptions wrapping. */ function copyStack(toErr, fromErr) { if (typeof fromErr?.stack !== "string" || typeof fromErr?.message !== "string") { return toErr; } // This is magic, the 1st line in stacktrace must be exactly "ExceptionType: // exception message\n", otherwise jest goes mad and prints the stacktrace // incorrectly (once from err.message and then once from err.stack). See also: // https://stackoverflow.com/questions/42754270/re-throwing-exception-in-nodejs-and-not-losing-stack-trace const fromMessageLines = fromErr.message.split("\n").length; toErr.stack = toErr.toString() + // original toErr message "\n" + fromErr.stack .split("\n") .slice(fromMessageLines) // skip prefix=fromErr.message in fromErr.stack .join("\n"); return toErr; } /** * Tries to minify a stacktrace by removing common parts of the paths. See unit * test with snapshot for examples. */ function minifyStack(stack, framesToPop) { return stack .replace(/^\w+:[ ]*\n/s, "") // remove "Error:" prefix .trim() .split("\n") .slice(framesToPop) .join("\n") .replace(/^\s+/gm, "") .replace(/^[^\n]+\(<anonymous>\)\n/gm, "") .replace(/(:\d+):\d+(?=[\n)])/gs, "$1") .replace(/^(at )\/.+\//gm, "$1") .replace(/^(at [^\n]+\()\/.+\//gm, "$1") .replace(/^(at )([^\n]+?) \((.+)\)/gm, "$1$3 ($2)"); } /** * A simple sequence generator which never returns the same value twice within * the same process. It's NOT random, NOT for cryptography, NOT stored (so * starts from scratch on a process restart) and is NOT shared with other * processes. */ function localUniqueInt() { return sequenceValue++; } let sequenceValue = 1; /** * The quickest string hasher. Don't use for crypto purposes! * https://medium.com/@chris_72272/what-is-the-fastest-node-js-hashing-algorithm-c15c1a0e164e */ function stringHash(s) { return (0, crypto_1.createHash)("sha1").update(s).digest("hex"); } /** * Used to calculate stable hashes of e.g. unique keys. */ function objectHash(obj) { return (0, object_hash_1.default)(obj, { algorithm: "sha1", encoding: "buffer", }); } /** * Similar to objectHash(), but uses JSON.stringify() under the hood, assuming * that it's faster than objectHash(). Also, doesn't throw when the object * contains bigint values (as opposed to JSON.stringify()). */ function jsonHash(obj) { return stringHash(JSON.stringify(obj, (_, value) => typeof value === "bigint" ? value.toString() : value)); } /** * Indents each line of the text with 2 spaces. */ function indent(message) { return message.replace(/^/gm, " "); } /** * Adds text suffixes to the sentence (typically, to an error message). */ function addSentenceSuffixes(sentence, ...suffixes) { const compacted = (0, compact_1.default)(suffixes); if (compacted.length === 0) { return sentence; } const suffix = compacted .filter((suffix) => !sentence.endsWith(suffix)) .join(""); return suffix.startsWith("\n") ? sentence + suffix : sentence.trimEnd().replace(/[.!?]+$/s, "") + suffix; } /** * Returns the 1st line of the message. */ function firstLine(message) { return (typeof message === "string" ? message.replace(/\n.*/s, "") : message); } /** * A shorthand for inspect() in compact/no-break mode. */ function inspectCompact(obj) { return (0, util_1.inspect)(obj, { compact: true, breakLength: Infinity }).replace(/^([[])\s+|\s+([\]])$/gs, (_, $1, $2) => $1 || $2); } /** * Prepares something which is claimed to be an ID for debug printing in e.g. * exception messages. We replace all non-ASCII characters to their \u * representations. */ function sanitizeIDForDebugPrinting(idIn) { const MAX_LEN = 32; const id = "" + idIn; const value = id .substring(0, MAX_LEN) // We want to use control characters in this regex. // eslint-disable-next-line no-control-regex .replace(/[^\x1F-\x7F]/g, (v) => "\\u" + v.charCodeAt(0)) + (id.length > MAX_LEN ? "..." : ""); return value === "" ? '""' : value; } /** * Throws if the value passed is null or undefined. */ function nullthrows(x, message) { if (x !== null && x !== undefined) { return x; } if (typeof message === "function") { message = message(); } const error = message instanceof Error ? message : Error(message ?? `Got unexpected ${x} in nullthrows()`); Error.captureStackTrace(error, nullthrows); throw error; } /** * Two modes: * 1. If an async (or sync) function is passed, spawns it in background and * doesn't await for its termination. * 2. If a Promise is passed, lets it continue executing, doesn't await on it. * * Useful when we want to launch a function "in the air", "hanging in nowhere", * and make no-misused-promises and no-floating-promises rules happy with it. An * example is some legacy callback-based API (e.g. chrome extension API) where * we want to pass an async function. * * It's like an analog of "async on intent" comment in the code. */ function runInVoid(funcOrPromise) { if (funcOrPromise instanceof Function) { // eslint-disable-next-line @typescript-eslint/no-floating-promises funcOrPromise(); } else { // do nothing, our Promise is already hanging in nowhere } } /** * A typesafe-way to invariant the object's key presence and being * non-undefined. It is not always working for union types: sometimes it asserts * the value of the key to be "any". It also doesn't remove "undefined" from the * type of the value. */ function hasKey(k, o) { return (!!o && (typeof o === "object" || typeof o === "function") && k in o && o[k] !== undefined); } /** * Same as Object.entries(), but returns strongly-typed entries. */ function entries(obj) { return Object.entries(obj); } /** * If the passed value is a function, calls it; otherwise, returns it intact. */ function maybeCall(valueOrFn) { return typeof valueOrFn === "function" || valueOrFn instanceof Function ? valueOrFn() : valueOrFn; } /** * Same as maybeCall(), but for MaybeAsyncCallable. */ async function maybeAsyncCall(valueOrFn) { return typeof valueOrFn === "function" || valueOrFn instanceof Function ? valueOrFn() : valueOrFn; } //# sourceMappingURL=misc.js.map