186 lines
7 KiB
JavaScript
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;
|