UNPKG

inngest

Version:

Official SDK for Inngest.com. Inngest is the reliability layer for modern applications. Inngest combines durable execution, events, and queues into a zero-infra platform with built-in observability.

398 lines 16.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.functionStoppedRunningErr = exports.rethrowError = exports.OutgoingResultError = exports.fixEventKeyMissingSteps = exports.prettyError = exports.getErrorMessage = exports.minifyPrettyError = exports.prettyErrorSplitter = exports.ErrCode = exports.deserializeError = exports.isSerializedError = exports.serializeError = void 0; const chalk_1 = __importDefault(require("chalk")); const json_stringify_safe_1 = __importDefault(require("json-stringify-safe")); const serialize_error_cjs_1 = require("serialize-error-cjs"); const strip_ansi_1 = __importDefault(require("strip-ansi")); const zod_1 = require("zod"); const NonRetriableError_js_1 = require("../components/NonRetriableError.js"); const SERIALIZED_KEY = "__serialized"; const SERIALIZED_VALUE = true; /** * Add first-class support for certain errors that we control, in addition to * built-in errors such as `TypeError`. * * Adding these allows these non-standard errors to be correctly serialized, * sent to Inngest, then deserialized back into the correct error type for users * to react to correctly. * * Note that these errors only support `message?: string | undefined` as the * input; more custom errors are not supported with this current strategy. */ serialize_error_cjs_1.errorConstructors.set("NonRetriableError", NonRetriableError_js_1.NonRetriableError); /** * Serialise an error to a serialized JSON string. * * Errors do not serialise nicely to JSON, so we use this function to convert * them to a serialized JSON string. Doing this is also non-trivial for some * errors, so we use the `serialize-error` package to do it for us. * * See {@link https://www.npmjs.com/package/serialize-error} * * This function is a small wrapper around that package to also add a `type` * property to the serialised error, so that we can distinguish between * serialised errors and other objects. * * Will not reserialise existing serialised errors. */ const serializeError = ( /** * The suspected error to serialize. */ subject, /** * If `true` and the error is not serializable, will return the original value * as `unknown` instead of coercing it to a serialized error. */ allowUnknown = false) => { try { // Try to understand if this is already done. // Will handle stringified errors. const existingSerializedError = (0, exports.isSerializedError)(subject); if (existingSerializedError) { return existingSerializedError; } if (typeof subject === "object" && subject !== null) { // Is an object, so let's try and serialize it. const serializedErr = (0, serialize_error_cjs_1.serializeError)(subject); // Not a proper error was caught, so give us a chance to return `unknown`. if (!serializedErr.name && allowUnknown) { return subject; } // Serialization can succeed but assign no name or message, so we'll // map over the result here to ensure we have everything. // We'll just stringify the entire subject for the message, as this at // least provides some context for the user. const ret = Object.assign(Object.assign({}, serializedErr), { name: serializedErr.name || "Error", message: serializedErr.message || (0, json_stringify_safe_1.default)(subject) || "Unknown error; error serialization could not find a message.", stack: serializedErr.stack || "", [SERIALIZED_KEY]: SERIALIZED_VALUE }); // If we have a cause, make sure we recursively serialize them too. We are // lighter with causes though; attempt to recursively serialize them, but // stop if we find something that doesn't work and just return `unknown`. let target = ret; const maxDepth = 5; for (let i = 0; i < maxDepth; i++) { if (typeof target === "object" && target !== null && "cause" in target && target.cause) { target = target.cause = (0, exports.serializeError)(target.cause, true); continue; } break; } return ret; } // If it's not an object, it's hard to parse this as an Error. In this case, // we'll throw an error to start attempting backup strategies. throw new Error("Error is not an object; strange throw value."); } catch (err) { if (allowUnknown) { // If we are allowed to return unknown, we'll just return the original // value. return subject; } try { // If serialization fails, fall back to a regular Error and use the // original object as the message for an Error. We don't know what this // object looks like, so we can't do anything else with it. return Object.assign(Object.assign({}, (0, exports.serializeError)(new Error(typeof subject === "string" ? subject : (0, json_stringify_safe_1.default)(subject)), false)), { // Remove the stack; it's not relevant here stack: "", [SERIALIZED_KEY]: SERIALIZED_VALUE }); } catch (err) { // If this failed, then stringifying the object also failed, so we'll just // return a completely generic error. // Failing to stringify the object is very unlikely. return { name: "Could not serialize source error", message: "Serializing the source error failed.", stack: "", [SERIALIZED_KEY]: SERIALIZED_VALUE, }; } } }; exports.serializeError = serializeError; /** * Check if an object or a string is a serialised error created by * {@link serializeError}. */ const isSerializedError = (value) => { try { if (typeof value === "string") { const parsed = zod_1.z .object({ [SERIALIZED_KEY]: zod_1.z.literal(SERIALIZED_VALUE), name: zod_1.z.enum([...Array.from(serialize_error_cjs_1.errorConstructors.keys())]), message: zod_1.z.string(), stack: zod_1.z.string(), }) .passthrough() .safeParse(JSON.parse(value)); if (parsed.success) { return parsed.data; } } if (typeof value === "object" && value !== null) { const objIsSerializedErr = Object.prototype.hasOwnProperty.call(value, SERIALIZED_KEY) && // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access value[SERIALIZED_KEY] === SERIALIZED_VALUE; if (objIsSerializedErr) { return value; } } } catch (_a) { // no-op; we'll return undefined if parsing failed, as it isn't a serialized // error } }; exports.isSerializedError = isSerializedError; /** * Deserialise an error created by {@link serializeError}. * * Ensures we only deserialise errors that meet a minimum level of * applicability, inclusive of error handling to ensure that badly serialized * errors are still handled. */ const deserializeError = (subject, allowUnknown = false) => { const requiredFields = ["name", "message"]; try { const hasRequiredFields = requiredFields.every((field) => { return Object.prototype.hasOwnProperty.call(subject, field); }); if (!hasRequiredFields) { throw new Error(); } const deserializedErr = (0, serialize_error_cjs_1.deserializeError)(subject); if ("cause" in deserializedErr) { deserializedErr.cause = (0, exports.deserializeError)(deserializedErr.cause, true); } return deserializedErr; } catch (_a) { if (allowUnknown) { // If we are allowed to return unknown, we'll just return the original // value. return subject; } const err = new Error("Unknown error; could not reserialize"); /** * Remove the stack so that it's not misleadingly shown as the Inngest * internals. */ err.stack = undefined; return err; } }; exports.deserializeError = deserializeError; var ErrCode; (function (ErrCode) { ErrCode["NESTING_STEPS"] = "NESTING_STEPS"; /** * Legacy v0 execution error code for when a function has changed and no * longer matches its in-progress state. * * @deprecated Not for use in latest execution method. */ ErrCode["NON_DETERMINISTIC_FUNCTION"] = "NON_DETERMINISTIC_FUNCTION"; /** * Legacy v0 execution error code for when a function is found to be using * async actions after memoziation has occurred, which v0 doesn't support. * * @deprecated Not for use in latest execution method. */ ErrCode["ASYNC_DETECTED_AFTER_MEMOIZATION"] = "ASYNC_DETECTED_AFTER_MEMOIZATION"; /** * Legacy v0 execution error code for when a function is found to be using * steps after a non-step async action has occurred. * * @deprecated Not for use in latest execution method. */ ErrCode["STEP_USED_AFTER_ASYNC"] = "STEP_USED_AFTER_ASYNC"; ErrCode["AUTOMATIC_PARALLEL_INDEXING"] = "AUTOMATIC_PARALLEL_INDEXING"; })(ErrCode || (exports.ErrCode = ErrCode = {})); exports.prettyErrorSplitter = "================================================="; /** * Given an unknown `err`, mutate it to minify any pretty errors that it * contains. */ const minifyPrettyError = (err) => { var _a, _b, _c, _d; try { if (!isError(err)) { return err; } const isPrettyError = err.message.includes(exports.prettyErrorSplitter); if (!isPrettyError) { return err; } const sanitizedMessage = (0, strip_ansi_1.default)(err.message); const message = ((_b = (_a = sanitizedMessage.split(" ")[1]) === null || _a === void 0 ? void 0 : _a.split("\n")[0]) === null || _b === void 0 ? void 0 : _b.trim()) || err.message; const code = ((_d = (_c = sanitizedMessage.split("\n\nCode: ")[1]) === null || _c === void 0 ? void 0 : _c.split("\n\n")[0]) === null || _d === void 0 ? void 0 : _d.trim()) || undefined; err.message = [code, message].filter(Boolean).join(" - "); if (err.stack) { const sanitizedStack = (0, strip_ansi_1.default)(err.stack); const stackRest = sanitizedStack .split(`${exports.prettyErrorSplitter}\n`) .slice(2) .join("\n"); err.stack = `${err.name}: ${err.message}\n${stackRest}`; } return err; } catch (noopErr) { return err; } }; exports.minifyPrettyError = minifyPrettyError; /** * Given an `err`, return a boolean representing whether it is in the shape of * an `Error` or not. */ const isError = (err) => { try { if (err instanceof Error) { return true; } const hasName = Object.prototype.hasOwnProperty.call(err, "name"); const hasMessage = Object.prototype.hasOwnProperty.call(err, "message"); return hasName && hasMessage; } catch (noopErr) { return false; } }; /** * Given an `unknown` object, retrieve the `message` property from it, or fall * back to the `fallback` string if it doesn't exist or is empty. */ const getErrorMessage = (err, fallback) => { const { message } = zod_1.z .object({ message: zod_1.z.string().min(1) }) .catch({ message: fallback }) .parse(err); return message; }; exports.getErrorMessage = getErrorMessage; /** * Given a {@link PrettyError}, return a nicely-formatted string ready to log * or throw. * * Useful for ensuring that errors are logged in a consistent, helpful format * across the SDK by prompting for key pieces of information. */ const prettyError = ({ type = "error", whatHappened, otherwise, reassurance, toFixNow, why, consequences, stack, code, }) => { var _a, _b; const { icon, colorFn } = { error: { icon: "❌", colorFn: chalk_1.default.red }, warn: { icon: "⚠️", colorFn: chalk_1.default.yellow }, }[type]; let header = `${icon} ${chalk_1.default.bold.underline(whatHappened.trim())}`; if (stack) { header += "\n" + [...(((_a = new Error().stack) === null || _a === void 0 ? void 0 : _a.split("\n").slice(1).filter(Boolean)) || [])].join("\n"); } let toFixNowStr = (_b = (Array.isArray(toFixNow) ? toFixNow .map((s) => s.trim()) .filter(Boolean) .map((s, i) => `\t${i + 1}. ${s}`) .join("\n") : toFixNow === null || toFixNow === void 0 ? void 0 : toFixNow.trim())) !== null && _b !== void 0 ? _b : ""; if (Array.isArray(toFixNow) && toFixNowStr) { toFixNowStr = `To fix this, you can take one of the following courses of action:\n\n${toFixNowStr}`; } let body = [reassurance === null || reassurance === void 0 ? void 0 : reassurance.trim(), why === null || why === void 0 ? void 0 : why.trim(), consequences === null || consequences === void 0 ? void 0 : consequences.trim()] .filter(Boolean) .join(" "); body += body ? `\n\n${toFixNowStr}` : toFixNowStr; const trailer = [otherwise === null || otherwise === void 0 ? void 0 : otherwise.trim()].filter(Boolean).join(" "); const message = [ exports.prettyErrorSplitter, header, body, trailer, code ? `Code: ${code}` : "", exports.prettyErrorSplitter, ] .filter(Boolean) .join("\n\n"); return colorFn(message); }; exports.prettyError = prettyError; exports.fixEventKeyMissingSteps = [ "Set the `INNGEST_EVENT_KEY` environment variable", `Pass a key to the \`new Inngest()\` constructor using the \`${"eventKey"}\` option`, `Use \`inngest.${"setEventKey"}()\` at runtime`, ]; /** * An error that, when thrown, indicates internally that an outgoing operation * contains an error. * * We use this because serialized `data` sent back to Inngest may differ from * the error instance itself due to middleware. * * @internal */ class OutgoingResultError extends Error { constructor(result) { super("OutgoingOpError"); this.result = result; } } exports.OutgoingResultError = OutgoingResultError; /** * Create a function that will rethrow an error with a prefix added to the * message. * * Useful for adding context to errors that are rethrown. * * @example * ```ts * await doSomeAction().catch(rethrowError("Failed to do some action")); * ``` */ // eslint-disable-next-line @typescript-eslint/no-explicit-any const rethrowError = (prefix) => { return (err) => { try { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/restrict-template-expressions err.message && (err.message = `${prefix}; ${err.message}`); } catch (noopErr) { // no-op } finally { // eslint-disable-next-line no-unsafe-finally throw err; } }; }; exports.rethrowError = rethrowError; /** * Legacy v0 execution error for functions that don't support mixing steps and * regular async actions. */ const functionStoppedRunningErr = (code) => { return (0, exports.prettyError)({ whatHappened: "Your function was stopped from running", why: "We detected a mix of asynchronous logic, some using step tooling and some not.", consequences: "This can cause unexpected behaviour when a function is paused and resumed and is therefore strongly discouraged; we stopped your function to ensure nothing unexpected happened!", stack: true, toFixNow: "Ensure that your function is either entirely step-based or entirely non-step-based, by either wrapping all asynchronous logic in `step.run()` calls or by removing all `step.*()` calls.", otherwise: "For more information on why step functions work in this manner, see https://www.inngest.com/docs/functions/multi-step#gotchas", code, }); }; exports.functionStoppedRunningErr = functionStoppedRunningErr; //# sourceMappingURL=errors.js.map