morphit/node_modules/@beblurt/dblurt/lib/errors.js

186 lines
7 KiB
JavaScript

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.rpcErrorFromResponse = exports.classifyError = exports.isDBlurtError = exports.RpcApplicationError = exports.ValidationError = exports.SerializationError = exports.DBlurtError = void 0;
const utils_1 = require("./utils");
const defaultMetadata = {
category: 'unknown',
code: 'DBLURT_UNKNOWN',
retryable: false
};
/** Base class for typed dblurt errors with non-enumerable metadata. */
class DBlurtError extends Error {
constructor(name, message, options = {}) {
const fullMessage = options.cause ? `${message}: ${options.cause.message}` : message;
super(fullMessage);
this.dblurt_error = true;
delete this.message;
this.name = name;
const metadata = options.metadata || defaultMetadata;
Object.defineProperties(this, {
category: { configurable: true, enumerable: false, value: metadata.category },
code: { configurable: true, enumerable: false, value: metadata.code },
dblurt_error: { configurable: true, enumerable: false, value: true },
metadata: { configurable: true, enumerable: false, value: metadata },
retryable: { configurable: true, enumerable: false, value: metadata.retryable }
});
this.jse_shortmsg = message;
if (options.cause) {
this.jse_cause = options.cause;
}
this.jse_info = options.info || {};
Object.defineProperty(this, 'message', {
configurable: true,
enumerable: true,
value: fullMessage,
writable: true
});
if (Error.captureStackTrace) {
;
Error.captureStackTrace(this, this.constructor);
}
}
cause() {
return this.jse_cause;
}
}
exports.DBlurtError = DBlurtError;
/** Error raised when transaction or operation serialization fails. */
class SerializationError extends DBlurtError {
constructor(cause) {
super('SerializationError', 'Unable to serialize transaction', {
cause,
metadata: {
category: 'serialization',
code: 'DBLURT_SERIALIZATION',
retryable: false
}
});
}
}
exports.SerializationError = SerializationError;
/** Error raised when SDK-side input validation fails before signing/broadcast. */
class ValidationError extends DBlurtError {
constructor(message, context = {}) {
super('ValidationError', message, {
metadata: {
category: 'validation',
code: 'DBLURT_VALIDATION',
retryable: false,
...(context.field ? { field: context.field } : {}),
...(context.path ? { path: context.path } : {})
}
});
}
}
exports.ValidationError = ValidationError;
/** Error raised for JSON-RPC application errors returned by a Blurt RPC endpoint. */
class RpcApplicationError extends DBlurtError {
constructor(message, info, rpcCode) {
super('RPCError', message, {
info,
metadata: {
category: 'rpc_application',
code: 'DBLURT_RPC_APPLICATION',
retryable: false,
...(typeof rpcCode === 'number' ? { rpc_code: rpcCode } : {}),
...(info !== undefined ? { rpc_data: info } : {})
}
});
}
}
exports.RpcApplicationError = RpcApplicationError;
const formatValue = (value) => {
switch (typeof value) {
case 'object':
return JSON.stringify(value);
default:
return String(value);
}
};
const getErrorCode = (error) => {
return error && typeof error.code === 'string' ? error.code : undefined;
};
const getErrorName = (error) => {
return error && typeof error.name === 'string' ? error.name : undefined;
};
const getErrorMessage = (error) => {
return error && typeof error.message === 'string' ? error.message : undefined;
};
const isAbortLikeError = (error) => {
return getErrorName(error) === 'AbortError' || getErrorCode(error) === 'ABORT_ERR';
};
const transportRetryableCodes = ['ENOTFOUND', 'ECONNREFUSED', 'CERT_HAS_EXPIRED', 'EHOSTUNREACH'];
/** Return true when an unknown error value carries dblurt typed error metadata. */
const isDBlurtError = (error) => {
return Boolean(error && error.dblurt_error === true && error.metadata);
};
exports.isDBlurtError = isDBlurtError;
/**
* Classify an unknown error value without parsing human-readable messages in app code.
*
* Existing {@link DBlurtError} instances return their embedded metadata. Abort-like
* errors are classified as retryable timeouts, selected network failures as
* transport errors and unrecognized values as non-retryable unknown errors.
*/
const classifyError = (error) => {
if ((0, exports.isDBlurtError)(error)) {
return error.metadata;
}
const causeCode = getErrorCode(error);
const causeName = getErrorName(error);
const causeMessage = getErrorMessage(error);
if (isAbortLikeError(error)) {
return {
category: 'timeout',
code: 'DBLURT_TIMEOUT',
retryable: true,
...(causeName ? { cause_name: causeName } : {}),
...(causeMessage ? { cause_message: causeMessage } : {})
};
}
if (causeCode || (causeMessage && /^HTTP \d+:/i.test(causeMessage))) {
const httpStatus = causeMessage && /^HTTP (\d+):/i.exec(causeMessage);
const retryable = causeCode
? transportRetryableCodes.some(code => causeCode.includes(code))
: Boolean(httpStatus && Number(httpStatus[1]) >= 500);
return {
category: 'transport',
code: 'DBLURT_TRANSPORT',
retryable,
...(causeCode ? { cause_code: causeCode } : {}),
...(causeMessage ? { cause_message: causeMessage } : {})
};
}
return {
category: 'unknown',
code: 'DBLURT_UNKNOWN',
retryable: false,
...(causeName ? { cause_name: causeName } : {}),
...(causeMessage ? { cause_message: causeMessage } : {})
};
};
exports.classifyError = classifyError;
const rpcErrorFromResponse = (error) => {
const { data } = error;
let { message } = error;
if (data && data.stack && data.stack.length > 0) {
const top = data.stack[0];
const topData = (0, utils_1.copy)(top.data);
message = top.format.replace(/\$\{([a-z_]+)\}/gi, (match, key) => {
let rv = match;
if (topData[key]) {
rv = formatValue(topData[key]);
delete topData[key];
}
return rv;
});
const unformattedData = Object.keys(topData)
.map(key => ({ key, value: formatValue(topData[key]) }))
.map(item => `${item.key}=${item.value}`);
if (unformattedData.length > 0) {
message += ' ' + unformattedData.join(' ');
}
}
return new RpcApplicationError(message, data, error.code);
};
exports.rpcErrorFromResponse = rpcErrorFromResponse;