UNPKG

ses

Version:

Hardened JavaScript for Fearless Cooperation

196 lines (190 loc) 6.13 kB
// @ts-check import { Set, String, isArray, arrayJoin, arraySlice, arraySort, arrayMap, keys, fromEntries, freeze, is, isError, setAdd, setHas, stringIncludes, stringStartsWith, stringifyJson, toStringTagSymbol, } from '../commons.js'; /** @import {StringablePayload} from '../../types.js' */ /** * Joins English terms with commas and an optional conjunction. * * @param {(string | StringablePayload)[]} terms * @param {"and" | "or"} conjunction */ export const enJoin = (terms, conjunction) => { if (terms.length === 0) { return '(none)'; } else if (terms.length === 1) { return terms[0]; } else if (terms.length === 2) { const [first, second] = terms; return `${first} ${conjunction} ${second}`; } else { return `${arrayJoin(arraySlice(terms, 0, -1), ', ')}, ${conjunction} ${ terms[terms.length - 1] }`; } }; /** * Prepend the correct indefinite article onto a noun, typically a typeof * result, e.g., "an object" vs. "a number" * * @param {string} str The noun to prepend * @returns {string} The noun prepended with a/an */ const an = str => { str = `${str}`; if (str.length >= 1 && stringIncludes('aeiouAEIOU', str[0])) { return `an ${str}`; } return `a ${str}`; }; freeze(an); export { an }; /** * Like `JSON.stringify` but does not blow up if given a cycle or a bigint. * This is not * intended to be a serialization to support any useful unserialization, * or any programmatic use of the resulting string. The string is intended * *only* for showing a human under benign conditions, in order to be * informative enough for some * logging purposes. As such, this `bestEffortStringify` has an * imprecise specification and may change over time. * * The current `bestEffortStringify` possibly emits too many "seen" * markings: Not only for cycles, but also for repeated subtrees by * object identity. * * As a best effort only for diagnostic interpretation by humans, * `bestEffortStringify` also turns various cases that normal * `JSON.stringify` skips or errors on, like `undefined` or bigints, * into strings that convey their meaning. To distinguish this from * strings in the input, these synthesized strings always begin and * end with square brackets. To distinguish those strings from an * input string with square brackets, and input string that starts * with an open square bracket `[` is itself placed in square brackets. * * @param {any} payload * @param {(string|number)=} spaces * @returns {string} */ const bestEffortStringify = (payload, spaces = undefined) => { const seenSet = new Set(); const replacer = (_, val) => { switch (typeof val) { case 'object': { if (val === null) { return null; } if (setHas(seenSet, val)) { return '[Seen]'; } setAdd(seenSet, val); if (isError(val)) { return `[${val.name}: ${val.message}]`; } if (toStringTagSymbol in val) { // For the built-ins that have or inherit a `Symbol.toStringTag`-named // property, most of them inherit the default `toString` method, // which will print in a similar manner: `"[object Foo]"` vs // `"[Foo]"`. The exceptions are // * `Symbol.prototype`, `BigInt.prototype`, `String.prototype` // which don't matter to us since we handle primitives // separately and we don't care about primitive wrapper objects. // * TODO // `Date.prototype`, `TypedArray.prototype`. // Hmmm, we probably should make special cases for these. We're // not using these yet, so it's not urgent. But others will run // into these. // // Once #2018 is closed, the only objects in our code that have or // inherit a `Symbol.toStringTag`-named property are remotables // or their remote presences. // This printing will do a good job for these without // violating abstraction layering. This behavior makes sense // purely in terms of JavaScript concepts. That's some of the // motivation for choosing that representation of remotables // and their remote presences in the first place. return `[${val[toStringTagSymbol]}]`; } if (isArray(val)) { return val; } const names = keys(val); if (names.length < 2) { return val; } let sorted = true; for (let i = 1; i < names.length; i += 1) { if (names[i - 1] >= names[i]) { sorted = false; break; } } if (sorted) { return val; } arraySort(names); const entries = arrayMap(names, name => [name, val[name]]); return fromEntries(entries); } case 'function': { return `[Function ${val.name || '<anon>'}]`; } case 'string': { if (stringStartsWith(val, '[')) { return `[${val}]`; } return val; } case 'undefined': case 'symbol': { return `[${String(val)}]`; } case 'bigint': { return `[${val}n]`; } case 'number': { if (is(val, NaN)) { return '[NaN]'; } else if (val === Infinity) { return '[Infinity]'; } else if (val === -Infinity) { return '[-Infinity]'; } return val; } default: { return val; } } }; try { return stringifyJson(payload, replacer, spaces); } catch (_err) { // Don't do anything more fancy here if there is any // chance that might throw, unless you surround that // with another try-catch-recovery. For example, // the caught thing might be a proxy or other exotic // object rather than an error. The proxy might throw // whenever it is possible for it to. return '[Something that failed to stringify]'; } }; freeze(bestEffortStringify); export { bestEffortStringify };