@clickup/ent-framework
Version:
A PostgreSQL graph-database-alike library with microsharding and row-level security
297 lines • 10.7 kB
JavaScript
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
;