@orchard9ai/error-handling
Version:
Federated error handling package with go-core-http-toolkit format support and logging integration
1,566 lines (1,556 loc) • 45.8 kB
JavaScript
// src/core/ErrorHandler.ts
import { createLogger } from "@orchard9ai/logging";
import { ErrorCode } from "@orchard9ai/types";
// src/types/index.ts
var ErrorSeverity = /* @__PURE__ */ ((ErrorSeverity2) => {
ErrorSeverity2["Low"] = "low";
ErrorSeverity2["Medium"] = "medium";
ErrorSeverity2["High"] = "high";
ErrorSeverity2["Critical"] = "critical";
return ErrorSeverity2;
})(ErrorSeverity || {});
var ErrorCategory = /* @__PURE__ */ ((ErrorCategory2) => {
ErrorCategory2["Network"] = "network";
ErrorCategory2["Auth"] = "auth";
ErrorCategory2["Validation"] = "validation";
ErrorCategory2["Business"] = "business";
ErrorCategory2["System"] = "system";
ErrorCategory2["Unknown"] = "unknown";
return ErrorCategory2;
})(ErrorCategory || {});
// src/core/ErrorHandler.ts
var ErrorHandler = class {
constructor(config = {}) {
this.config = {
enableLogging: true,
enableMonitoring: true,
defaultMessages: this.getDefaultMessages(),
transformers: {},
loggerName: "error-handler",
...config
};
this.logger = createLogger(this.config.loggerName);
}
/**
* Handle API error from go-core-http-toolkit format
*/
handleApiError(error, context = {}) {
const severity = this.determineSeverity(error);
const category = this.categorizeError(error);
if (this.config.enableLogging) {
this.logError(error, context, severity, category);
}
return this.transformForDisplay(error);
}
/**
* Handle network errors
*/
handleNetworkError(error, context = {}) {
const apiError = {
error: "Network connection failed",
code: "NETWORK_ERROR",
details: {
message: error.message,
name: error.name
}
};
return this.handleApiError(apiError, context);
}
/**
* Handle validation errors
*/
handleValidationError(fields, context = {}) {
const apiError = {
error: "Validation failed",
code: "VALIDATION_ERROR",
details: fields
};
return this.handleApiError(apiError, context);
}
/**
* Transform API error to display-friendly format
*/
transformForDisplay(error) {
if (error.code && this.config.transformers && this.config.transformers[error.code]) {
return this.config.transformers[error.code](error);
}
const type = this.getDisplayType(error);
const retryable = this.isRetryable(error);
return {
title: this.getDisplayTitle(error),
message: error.error,
type,
retryable,
fieldErrors: error.details || {},
actions: this.getErrorActions(error, retryable)
};
}
/**
* Register custom error transformer
*/
registerTransformer(code, transformer) {
this.config.transformers[code] = transformer;
}
/**
* Check if error is retryable
*/
isRetryable(error) {
const retryableCodes = [
"NETWORK_ERROR",
"TIMEOUT",
"SERVICE_UNAVAILABLE",
"INTERNAL_ERROR",
"RATE_LIMIT_EXCEEDED"
];
return retryableCodes.includes(error.code || "");
}
/**
* Determine error severity
*/
determineSeverity(error) {
if (!error.code)
return "medium" /* Medium */;
const criticalCodes = ["INTERNAL_ERROR", "SERVICE_UNAVAILABLE"];
const highCodes = ["UNAUTHORIZED", "FORBIDDEN", "NOT_FOUND"];
const lowCodes = ["VALIDATION_ERROR", "INVALID_INPUT"];
if (criticalCodes.includes(error.code))
return "critical" /* Critical */;
if (highCodes.includes(error.code))
return "high" /* High */;
if (lowCodes.includes(error.code))
return "low" /* Low */;
return "medium" /* Medium */;
}
/**
* Categorize error by type
*/
categorizeError(error) {
if (!error.code)
return "unknown" /* Unknown */;
const authCodes = ["UNAUTHORIZED", "FORBIDDEN", "TOKEN_EXPIRED", "TOKEN_INVALID"];
const validationCodes = ["VALIDATION_ERROR", "INVALID_INPUT", "MISSING_REQUIRED_FIELD"];
const networkCodes = ["NETWORK_ERROR", "TIMEOUT", "SERVICE_UNAVAILABLE"];
const systemCodes = ["INTERNAL_ERROR", "CONFIG_ERROR"];
if (authCodes.includes(error.code))
return "auth" /* Auth */;
if (validationCodes.includes(error.code))
return "validation" /* Validation */;
if (networkCodes.includes(error.code))
return "network" /* Network */;
if (systemCodes.includes(error.code))
return "system" /* System */;
return "business" /* Business */;
}
/**
* Log error with appropriate level
*/
logError(error, context, severity, category) {
const logData = {
error: error.error,
code: error.code,
details: error.details,
trace_id: error.trace_id,
category,
...context
};
switch (severity) {
case "critical" /* Critical */:
this.logger.error("Critical error occurred", logData);
break;
case "high" /* High */:
this.logger.error("High severity error", logData);
break;
case "medium" /* Medium */:
this.logger.warn("Medium severity error", logData);
break;
case "low" /* Low */:
this.logger.info("Low severity error", logData);
break;
}
}
/**
* Get display type from error
*/
getDisplayType(error) {
const severity = this.determineSeverity(error);
if (severity === "critical" /* Critical */ || severity === "high" /* High */) {
return "error";
}
if (severity === "medium" /* Medium */) {
return "warning";
}
return "info";
}
/**
* Get display title from error
*/
getDisplayTitle(error) {
if (error.code && this.config.defaultMessages && this.config.defaultMessages[error.code]) {
return this.config.defaultMessages[error.code];
}
if (error.code) {
return error.code.toLowerCase().split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
}
return "Error";
}
/**
* Get error actions based on error type
*/
getErrorActions(error, retryable) {
const actions = [];
if (retryable) {
actions.push({
label: "Retry",
handler: () => window.location.reload(),
type: "primary"
});
}
if (error.code === "UNAUTHORIZED") {
actions.push({
label: "Sign In",
handler: () => {
console.log("Navigate to sign in");
},
type: "primary"
});
}
return actions;
}
/**
* Default error messages
*/
getDefaultMessages() {
return {
[ErrorCode.UNAUTHORIZED]: "Authentication Required",
[ErrorCode.FORBIDDEN]: "Access Denied",
[ErrorCode.NOT_FOUND]: "Not Found",
[ErrorCode.VALIDATION_ERROR]: "Invalid Input",
[ErrorCode.RATE_LIMIT_EXCEEDED]: "Too Many Requests",
[ErrorCode.INTERNAL_ERROR]: "System Error",
[ErrorCode.SERVICE_UNAVAILABLE]: "Service Unavailable",
[ErrorCode.TIMEOUT]: "Request Timeout",
"NETWORK_ERROR": "Network Error"
};
}
};
// src/utils/sanitize.ts
var htmlEntities = {
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'",
"/": "/"
};
function escapeHtml(text) {
return String(text).replace(/[&<>"'\/]/g, (char) => htmlEntities[char] || char);
}
function sanitizeErrorMessage(message) {
if (typeof message !== "string") {
return escapeHtml(String(message));
}
const cleaned = message.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "").replace(/on\w+\s*=\s*["'][^"']*["']/gi, "").replace(/javascript:/gi, "");
return escapeHtml(cleaned);
}
function sanitizeErrorDetails(details) {
const sanitized = {};
const sensitiveKeys = ["password", "token", "secret", "api_key", "apiKey", "authorization"];
for (const [key, value] of Object.entries(details)) {
if (sensitiveKeys.some((sensitive) => key.toLowerCase().includes(sensitive))) {
continue;
}
sanitized[key] = sanitizeErrorMessage(value);
}
return sanitized;
}
// src/utils/errorParsers.ts
function parseHttpError(response, responseText) {
try {
if (responseText) {
const parsed = JSON.parse(responseText);
if (isApiError(parsed)) {
return parsed;
}
if (parsed.message) {
return {
error: parsed.message,
code: parsed.code || getCodeFromStatus(response.status)
};
}
}
} catch {
}
return {
error: getMessageFromStatus(response.status),
code: getCodeFromStatus(response.status)
};
}
function parseFetchError(error) {
if (error.name === "AbortError") {
return {
error: "Request was cancelled",
code: "REQUEST_CANCELLED"
};
}
if (error.name === "TypeError" && error.message.includes("fetch")) {
return {
error: "Network connection failed",
code: "NETWORK_ERROR",
details: {
originalMessage: error.message
}
};
}
return {
error: sanitizeErrorMessage(error.message || "An unknown error occurred"),
code: "UNKNOWN_ERROR",
details: {
name: sanitizeErrorMessage(error.name)
}
};
}
function parseUnknownError(error) {
if (error instanceof Error) {
return parseFetchError(error);
}
if (typeof error === "string") {
return {
error,
code: "STRING_ERROR"
};
}
if (typeof error === "object" && error !== null) {
const obj = error;
if (isApiError(obj)) {
return obj;
}
return {
error: obj["message"] || "An error occurred",
code: obj["code"] || "OBJECT_ERROR",
details: obj["details"] || {}
};
}
return {
error: "An unknown error occurred",
code: "UNKNOWN_ERROR",
details: {
type: typeof error,
value: String(error)
}
};
}
function isApiError(obj) {
return typeof obj === "object" && obj !== null && "error" in obj && typeof obj.error === "string";
}
function getCodeFromStatus(status) {
switch (status) {
case 400:
return "BAD_REQUEST";
case 401:
return "UNAUTHORIZED";
case 403:
return "FORBIDDEN";
case 404:
return "NOT_FOUND";
case 409:
return "CONFLICT";
case 422:
return "VALIDATION_ERROR";
case 429:
return "RATE_LIMIT_EXCEEDED";
case 500:
return "INTERNAL_ERROR";
case 502:
return "BAD_GATEWAY";
case 503:
return "SERVICE_UNAVAILABLE";
case 504:
return "TIMEOUT";
default:
return "HTTP_ERROR";
}
}
function getMessageFromStatus(status) {
switch (status) {
case 400:
return "Bad request";
case 401:
return "Authentication required";
case 403:
return "Access denied";
case 404:
return "Not found";
case 409:
return "Conflict";
case 422:
return "Validation failed";
case 429:
return "Too many requests";
case 500:
return "Internal server error";
case 502:
return "Bad gateway";
case 503:
return "Service unavailable";
case 504:
return "Request timeout";
default:
return `HTTP error ${status}`;
}
}
// src/utils/displayHelpers.ts
function createUserFriendlyMessage(error) {
const friendlyMessages = {
"UNAUTHORIZED": "Please sign in to continue",
"FORBIDDEN": "You don't have permission to perform this action",
"NOT_FOUND": "The requested item could not be found",
"VALIDATION_ERROR": "Please check your input and try again",
"RATE_LIMIT_EXCEEDED": "You're doing that too often. Please wait a moment",
"INTERNAL_ERROR": "Something went wrong on our end. Please try again",
"SERVICE_UNAVAILABLE": "This service is temporarily unavailable",
"NETWORK_ERROR": "Please check your internet connection",
"TIMEOUT": "The request took too long. Please try again"
};
if (error.code && friendlyMessages[error.code]) {
return friendlyMessages[error.code];
}
return humanizeErrorMessage(sanitizeErrorMessage(error.error));
}
function humanizeErrorMessage(message) {
const humanized = message.replace(/\b(HTTP|API|JSON|XML)\b/gi, "").replace(/\b(error|exception|failure)\b:?\s*/gi, "").replace(/\b(code|status)\b:?\s*\d+/gi, "").replace(/\bmust\b/gi, "should").replace(/\bcannot\b/gi, "can't").replace(/\binvalid\b/gi, "incorrect").trim();
return humanized.charAt(0).toUpperCase() + humanized.slice(1);
}
function createRetryAction(retryFn) {
return {
label: "Try Again",
handler: retryFn,
type: "primary"
};
}
function createRefreshAction() {
return {
label: "Refresh Page",
handler: () => window.location.reload(),
type: "secondary"
};
}
function createSupportAction(supportUrl = "mailto:support@example.com") {
return {
label: "Contact Support",
handler: () => {
window.open(supportUrl, "_blank");
},
type: "secondary"
};
}
function createDismissAction(dismissFn) {
return {
label: "Dismiss",
handler: dismissFn,
type: "secondary"
};
}
function formatFieldErrors(details) {
const formatted = {};
const sanitized = sanitizeErrorDetails(details);
for (const [field, message] of Object.entries(sanitized)) {
const readableField = field.split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
const readableMessage = humanizeErrorMessage(message);
formatted[field] = `${readableField}: ${readableMessage}`;
}
return formatted;
}
function getErrorIcon(type) {
switch (type) {
case "error":
return "\u274C";
case "warning":
return "\u26A0\uFE0F";
case "info":
return "\u2139\uFE0F";
default:
return "\u274C";
}
}
function getErrorColor(type) {
switch (type) {
case "error":
return "red";
case "warning":
return "orange";
case "info":
return "blue";
default:
return "red";
}
}
function truncateErrorMessage(message, maxLength = 100) {
if (message.length <= maxLength) {
return message;
}
return message.slice(0, maxLength - 3) + "...";
}
function createToastError(error) {
const message = createUserFriendlyMessage(error);
const type = error.code === "VALIDATION_ERROR" ? "warning" : "error";
return {
title: "Error",
message: truncateErrorMessage(message, 80),
type
};
}
function shouldShowToUser(error) {
const hiddenCodes = [
"INTERNAL_ERROR",
"CONFIG_ERROR",
"DATABASE_ERROR",
"UNKNOWN_ERROR"
];
return !hiddenCodes.includes(error.code || "");
}
// src/handlers/ApiErrorHandler.ts
var ApiErrorHandler = class extends ErrorHandler {
constructor(config = {}) {
super({
loggerName: "api-error-handler",
...config
});
}
/**
* Handle fetch API errors
*/
async handleFetchError(response, context = {}) {
let responseText;
try {
responseText = await response.text();
} catch {
responseText = "";
}
const apiError = parseHttpError(response, responseText);
const enrichedContext = {
...context,
action: "http_request",
metadata: {
url: response.url,
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries(response.headers.entries())
}
};
return this.handleApiError(apiError, enrichedContext);
}
/**
* Handle network errors (fetch failures)
*/
handleNetworkError(error, context = {}) {
const apiError = parseFetchError(error);
const enrichedContext = {
...context,
action: "network_request",
metadata: {
errorName: error.name,
errorMessage: error.message
}
};
return this.handleApiError(apiError, enrichedContext);
}
/**
* Handle unknown errors
*/
handleUnknownError(error, context = {}) {
const apiError = parseUnknownError(error);
const enrichedContext = {
...context,
action: "unknown_error",
metadata: {
errorType: typeof error
}
};
return this.handleApiError(apiError, enrichedContext);
}
/**
* Create a wrapper for fetch that handles errors
*/
createFetchWrapper(baseUrl = "") {
return async (input, init) => {
try {
const url = typeof input === "string" ? `${baseUrl}${input}` : input;
const response = await fetch(url, init);
if (!response.ok) {
const displayError = await this.handleFetchError(response, {
component: "fetch-wrapper",
action: "api-request"
});
const error = new ApiRequestError(
`HTTP ${response.status}: ${response.statusText}`,
response.status,
displayError
);
throw error;
}
return response;
} catch (error) {
if (error instanceof ApiRequestError) {
throw error;
}
const displayError = this.handleNetworkError(error, {
component: "fetch-wrapper",
action: "network-request"
});
throw new ApiRequestError(
error.message,
0,
displayError
);
}
};
}
/**
* Enhanced transform with API-specific logic
*/
transformForDisplay(error) {
const baseDisplay = super.transformForDisplay(error);
const actions = [...baseDisplay.actions || []];
if (error.code === "INTERNAL_ERROR" || error.code === "SERVICE_UNAVAILABLE") {
actions.push(createSupportAction());
}
if (baseDisplay.retryable) {
actions.unshift(createRetryAction(() => {
}));
}
return {
...baseDisplay,
message: createUserFriendlyMessage(error),
actions
};
}
};
var ApiRequestError = class extends Error {
constructor(message, statusCode, displayError) {
super(message);
this.name = "ApiRequestError";
this.statusCode = statusCode;
this.displayError = displayError;
}
};
var globalApiErrorHandler = null;
function getGlobalApiErrorHandler() {
if (!globalApiErrorHandler) {
globalApiErrorHandler = new ApiErrorHandler();
}
return globalApiErrorHandler;
}
function configureGlobalApiErrorHandler(config) {
globalApiErrorHandler = new ApiErrorHandler(config);
}
function handleApiError(error, context) {
return getGlobalApiErrorHandler().handleApiError(error, context);
}
async function handleFetchError(response, context) {
return getGlobalApiErrorHandler().handleFetchError(response, context);
}
// src/handlers/HttpClient.ts
var HttpClientError = class extends Error {
constructor(message, status, statusText, displayError, apiError, response) {
super(message);
this.name = "HttpClientError";
this.status = status;
this.statusText = statusText;
this.response = response;
this.displayError = displayError;
this.apiError = apiError;
}
};
var HttpClient = class {
constructor(config = {}) {
this.baseUrl = config.baseUrl || "";
this.defaultHeaders = {
"Content-Type": "application/json",
...config.defaultHeaders
};
this.timeout = config.timeout || 3e4;
this.enableRetry = config.enableRetry || false;
this.retryAttempts = config.retryAttempts || 3;
this.retryDelay = config.retryDelay || 1e3;
this.onError = config.onError;
this.showToasts = config.showToasts || true;
this.errorHandler = new ErrorHandler({
loggerName: "http-client",
...config.errorHandlerConfig
});
}
/**
* Make HTTP request
*/
async request(endpoint, config = {}) {
const { timeout = this.timeout, skipRetry = false, errorContext, ...fetchConfig } = config;
const url = this.buildUrl(endpoint);
const requestConfig = {
...fetchConfig,
headers: {
...this.defaultHeaders,
...fetchConfig.headers
}
};
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await this.executeRequest(url, {
...requestConfig,
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
const displayError = await this.handleHttpError(response, {
component: "http-client",
action: "request",
...errorContext
});
throw new HttpClientError(
`HTTP ${response.status}: ${response.statusText}`,
response.status,
response.statusText,
displayError,
displayError,
// Will be properly typed
response
);
}
const data = await this.parseResponse(response);
return {
data,
status: response.status,
statusText: response.statusText,
headers: response.headers,
url: response.url
};
} catch (error) {
clearTimeout(timeoutId);
if (error instanceof HttpClientError) {
this.handleError(error);
throw error;
}
const apiError = parseUnknownError(error);
const displayError = this.errorHandler.handleApiError(apiError, {
component: "http-client",
action: "network-request",
...errorContext
});
const httpError = new HttpClientError(
error.message,
0,
"Network Error",
displayError,
displayError
);
this.handleError(httpError);
throw httpError;
}
}
/**
* GET request
*/
async get(endpoint, config) {
return this.request(endpoint, { ...config, method: "GET" });
}
/**
* POST request
*/
async post(endpoint, data, config) {
return this.request(endpoint, {
...config,
method: "POST",
body: data ? JSON.stringify(data) : null
});
}
/**
* PUT request
*/
async put(endpoint, data, config) {
return this.request(endpoint, {
...config,
method: "PUT",
body: data ? JSON.stringify(data) : null
});
}
/**
* PATCH request
*/
async patch(endpoint, data, config) {
return this.request(endpoint, {
...config,
method: "PATCH",
body: data ? JSON.stringify(data) : null
});
}
/**
* DELETE request
*/
async delete(endpoint, config) {
return this.request(endpoint, { ...config, method: "DELETE" });
}
/**
* Execute request with optional retry logic
*/
async executeRequest(url, config) {
let lastError = null;
const maxAttempts = this.enableRetry ? this.retryAttempts : 1;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const response = await fetch(url, config);
if (response.status >= 400 && response.status < 500) {
if (response.status !== 408 && response.status !== 429) {
return response;
}
}
if (response.ok || attempt === maxAttempts) {
return response;
}
if (attempt < maxAttempts) {
await this.delay(this.retryDelay * attempt);
}
} catch (error) {
lastError = error;
if (error instanceof DOMException && error.name === "AbortError") {
throw error;
}
if (attempt < maxAttempts) {
await this.delay(this.retryDelay * attempt);
}
}
}
throw lastError || new Error("Request failed after retries");
}
/**
* Handle HTTP error responses
*/
async handleHttpError(response, context = {}) {
let responseText;
try {
responseText = await response.text();
} catch {
responseText = "";
}
let apiError;
try {
const errorData = JSON.parse(responseText);
const details = {};
if (errorData.details) {
Object.entries(errorData.details).forEach(([key, value]) => {
if (typeof value === "string") {
details[key] = value;
} else {
details[key] = String(value);
}
});
}
apiError = {
error: errorData.error || errorData.message,
code: errorData.code,
details: Object.keys(details).length > 0 ? details : void 0,
trace_id: errorData.correlation_id
};
} catch {
apiError = parseHttpError(response, responseText);
}
const enrichedContext = {
...context,
action: "http_request",
metadata: {
url: response.url,
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries(response.headers.entries())
}
};
return this.errorHandler.handleApiError(apiError, enrichedContext);
}
/**
* Parse response data
*/
async parseResponse(response) {
const contentType = response.headers.get("content-type");
if (contentType?.includes("application/json")) {
return response.json();
}
if (contentType?.includes("text/")) {
return response.text();
}
return response.blob();
}
/**
* Handle errors (logging, callbacks, toasts)
*/
handleError(error) {
if (this.onError) {
this.onError(error, error.displayError);
}
if (this.showToasts) {
this.showErrorToast(error.displayError);
}
}
/**
* Show error toast
*/
showErrorToast(displayError) {
}
/**
* Build full URL
*/
buildUrl(endpoint) {
if (endpoint.startsWith("http://") || endpoint.startsWith("https://")) {
return endpoint;
}
const base = this.baseUrl.endsWith("/") ? this.baseUrl.slice(0, -1) : this.baseUrl;
const path = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
return `${base}${path}`;
}
/**
* Delay helper for retry logic
*/
delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Update configuration
*/
configure(config) {
if (config.baseUrl !== void 0)
this.baseUrl = config.baseUrl;
if (config.defaultHeaders) {
this.defaultHeaders = { ...this.defaultHeaders, ...config.defaultHeaders };
}
if (config.timeout !== void 0)
this.timeout = config.timeout;
if (config.enableRetry !== void 0)
this.enableRetry = config.enableRetry;
if (config.retryAttempts !== void 0)
this.retryAttempts = config.retryAttempts;
if (config.retryDelay !== void 0)
this.retryDelay = config.retryDelay;
if (config.onError !== void 0)
this.onError = config.onError;
if (config.showToasts !== void 0)
this.showToasts = config.showToasts;
}
};
function createHttpClient(config) {
return new HttpClient(config);
}
var globalHttpClient = null;
function getGlobalHttpClient() {
if (!globalHttpClient) {
globalHttpClient = new HttpClient();
}
return globalHttpClient;
}
function configureGlobalHttpClient(config) {
globalHttpClient = new HttpClient(config);
}
// src/handlers/GlobalErrorHandler.ts
var GlobalErrorHandler = class {
constructor(config = {}) {
this.config = {
enabled: true,
showToasts: true,
logToConsole: true,
onError: config.onError,
onUnhandledRejection: config.onUnhandledRejection,
errorHandlerConfig: {},
ignoredErrors: [
"Script error.",
"Non-Error promise rejection captured",
"ResizeObserver loop limit exceeded"
],
maxErrorsPerMinute: 10,
...config
};
this.errorHandler = new ErrorHandler({
loggerName: "global-error-handler",
...this.config.errorHandlerConfig
});
this.errorCounts = [];
this.stats = {
totalErrors: 0,
errorsByType: {},
errorsByMinute: []
};
if (this.config.enabled) {
this.install();
}
}
/**
* Install global error handlers
*/
install() {
if (typeof window === "undefined") {
return;
}
this.originalErrorHandler = window.onerror;
this.originalRejectionHandler = window.onunhandledrejection;
window.onerror = (message, source, lineno, colno, error) => {
this.handleWindowError(message, source, lineno, colno, error);
if (this.originalErrorHandler) {
return this.originalErrorHandler(message, source, lineno, colno, error);
}
return false;
};
window.onunhandledrejection = (event) => {
this.handleUnhandledRejection(event);
if (this.originalRejectionHandler) {
return this.originalRejectionHandler.call(window, event);
}
};
this.cleanupInterval = window.setInterval(() => {
this.cleanupErrorCounts();
}, 6e4);
}
/**
* Uninstall global error handlers
*/
uninstall() {
if (typeof window === "undefined") {
return;
}
if (this.cleanupInterval) {
window.clearInterval(this.cleanupInterval);
this.cleanupInterval = void 0;
}
window.onerror = this.originalErrorHandler || null;
window.onunhandledrejection = this.originalRejectionHandler || null;
this.originalErrorHandler = null;
this.originalRejectionHandler = null;
}
/**
* Handle window errors
*/
handleWindowError(message, source, lineno, colno, error) {
if (!this.checkRateLimit()) {
return;
}
const errorMessage = typeof message === "string" ? message : error?.message || "Unknown error";
if (this.shouldIgnoreError(errorMessage)) {
return;
}
const context = {
component: "global-error-handler",
action: "window-error",
metadata: {
source,
lineno,
colno,
userAgent: navigator.userAgent,
url: window.location.href
}
};
const actualError = error || new Error(errorMessage);
const displayError = this.processError(actualError, context);
this.updateStats(actualError, context);
if (this.config.onError) {
this.config.onError(actualError, displayError, context);
}
if (this.config.showToasts) {
this.showErrorToast(displayError);
}
if (this.config.logToConsole) {
console.error("Global Error:", actualError, { context, displayError });
}
}
/**
* Handle unhandled promise rejections
*/
handleUnhandledRejection(event) {
if (!this.checkRateLimit()) {
return;
}
const reason = event.reason;
const errorMessage = reason instanceof Error ? reason.message : String(reason);
if (this.shouldIgnoreError(errorMessage)) {
return;
}
const context = {
component: "global-error-handler",
action: "unhandled-rejection",
metadata: {
promiseString: event.promise.toString(),
reasonType: typeof reason,
userAgent: navigator.userAgent,
url: window.location.href
}
};
const error = reason instanceof Error ? reason : new Error(String(reason));
const displayError = this.processError(error, context);
this.updateStats(error, context);
if (this.config.onUnhandledRejection) {
this.config.onUnhandledRejection(reason, event.promise, displayError);
}
if (this.config.onError) {
this.config.onError(error, displayError, context);
}
if (this.config.showToasts) {
this.showErrorToast(displayError);
}
if (this.config.logToConsole) {
console.error("Unhandled Promise Rejection:", reason, { context, displayError });
}
event.preventDefault();
}
/**
* Process error through error handler
*/
processError(error, context) {
const apiError = parseUnknownError(error);
return this.errorHandler.handleApiError(apiError, context);
}
/**
* Check if error should be ignored
*/
shouldIgnoreError(message) {
return this.config.ignoredErrors.some(
(ignored) => message.includes(ignored) || message === ignored
);
}
/**
* Check rate limiting
*/
checkRateLimit() {
const now = Date.now();
const oneMinuteAgo = now - 6e4;
this.errorCounts = this.errorCounts.filter((timestamp) => timestamp > oneMinuteAgo);
if (this.errorCounts.length >= this.config.maxErrorsPerMinute) {
return false;
}
this.errorCounts.push(now);
return true;
}
/**
* Update error statistics
*/
updateStats(error, context) {
this.stats.totalErrors++;
const errorType = error.constructor.name;
this.stats.errorsByType[errorType] = (this.stats.errorsByType[errorType] || 0) + 1;
this.stats.lastError = {
message: error.message,
timestamp: /* @__PURE__ */ new Date(),
context
};
const minute = Math.floor(Date.now() / 6e4);
if (this.stats.errorsByMinute.length === 0 || this.stats.errorsByMinute[this.stats.errorsByMinute.length - 1] !== minute) {
this.stats.errorsByMinute.push(minute);
}
}
/**
* Clean up old error counts
*/
cleanupErrorCounts() {
const oneMinuteAgo = Date.now() - 6e4;
this.errorCounts = this.errorCounts.filter((timestamp) => timestamp > oneMinuteAgo);
}
/**
* Show error toast
*/
showErrorToast(displayError) {
}
/**
* Get error statistics
*/
getStats() {
return { ...this.stats };
}
/**
* Reset error statistics
*/
resetStats() {
this.stats = {
totalErrors: 0,
errorsByType: {},
errorsByMinute: []
};
this.errorCounts = [];
}
/**
* Update configuration
*/
configure(config) {
this.config = { ...this.config, ...config };
if (config.enabled !== void 0) {
if (config.enabled) {
this.install();
} else {
this.uninstall();
}
}
}
/**
* Add error to ignore list
*/
addIgnoredError(pattern) {
if (!this.config.ignoredErrors.includes(pattern)) {
this.config.ignoredErrors.push(pattern);
}
}
/**
* Remove error from ignore list
*/
removeIgnoredError(pattern) {
const index = this.config.ignoredErrors.indexOf(pattern);
if (index > -1) {
this.config.ignoredErrors.splice(index, 1);
}
}
};
var globalErrorHandler = null;
function getGlobalErrorHandler() {
if (!globalErrorHandler) {
globalErrorHandler = new GlobalErrorHandler();
}
return globalErrorHandler;
}
function configureGlobalErrorHandling(config) {
globalErrorHandler = new GlobalErrorHandler(config);
return globalErrorHandler;
}
function installGlobalErrorHandling(config) {
return configureGlobalErrorHandling({ enabled: true, ...config });
}
// src/ui/ErrorToast.tsx
import * as React from "react";
import { jsx, jsxs } from "react/jsx-runtime";
var defaultToastConfig = {
autoHideDuration: 5e3,
position: "top-right",
maxToasts: 5,
animationDuration: 300
};
var ToastContext = React.createContext(null);
function useToast() {
const context = React.useContext(ToastContext);
if (!context) {
throw new Error("useToast must be used within a ToastProvider");
}
return context;
}
function ToastProvider({ children, config }) {
const [toasts, setToasts] = React.useState([]);
const [toastConfig, setToastConfig] = React.useState({
...defaultToastConfig,
...config
});
const showToast = React.useCallback((displayError) => {
const id = `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const toast = {
id,
displayError,
timestamp: Date.now()
};
setToasts((prevToasts) => {
const newToasts = [...prevToasts, toast];
if (newToasts.length > toastConfig.maxToasts) {
return newToasts.slice(-toastConfig.maxToasts);
}
return newToasts;
});
if (toastConfig.autoHideDuration > 0) {
setTimeout(() => {
hideToast(id);
}, toastConfig.autoHideDuration);
}
return id;
}, [toastConfig.maxToasts, toastConfig.autoHideDuration]);
const hideToast = React.useCallback((id) => {
setToasts(
(prevToasts) => prevToasts.map(
(toast) => toast.id === id ? { ...toast, dismissed: true } : toast
)
);
setTimeout(() => {
setToasts((prevToasts) => prevToasts.filter((toast) => toast.id !== id));
}, toastConfig.animationDuration);
}, [toastConfig.animationDuration]);
const clearAllToasts = React.useCallback(() => {
setToasts([]);
}, []);
const updateConfig = React.useCallback((newConfig) => {
setToastConfig((prev) => ({ ...prev, ...newConfig }));
}, []);
const contextValue = {
toasts,
showToast,
hideToast,
clearAllToasts,
config: toastConfig,
updateConfig
};
return /* @__PURE__ */ jsxs(ToastContext.Provider, { value: contextValue, children: [
children,
/* @__PURE__ */ jsx(ToastContainer, {})
] });
}
function ToastContainer() {
const { toasts, config } = useToast();
const positionClasses = {
"top-right": "top-4 right-4",
"top-left": "top-4 left-4",
"bottom-right": "bottom-4 right-4",
"bottom-left": "bottom-4 left-4",
"top-center": "top-4 left-1/2 transform -translate-x-1/2",
"bottom-center": "bottom-4 left-1/2 transform -translate-x-1/2"
};
return /* @__PURE__ */ jsx(
"div",
{
className: `fixed z-50 pointer-events-none ${positionClasses[config.position]}`,
style: { maxWidth: "400px", width: "100%" },
children: /* @__PURE__ */ jsx("div", { className: "space-y-2", children: toasts.map((toast) => /* @__PURE__ */ jsx(
ErrorToastItem,
{
toast,
animationDuration: config.animationDuration
},
toast.id
)) })
}
);
}
function ErrorToastItem({ toast, animationDuration }) {
const { hideToast } = useToast();
const { displayError, dismissed } = toast;
const handleAction = (action) => {
action.handler();
hideToast(toast.id);
};
const handleDismiss = () => {
hideToast(toast.id);
};
const typeStyles = {
error: "bg-error/10 border-error/20 text-error",
warning: "bg-warning/10 border-warning/20 text-warning",
info: "bg-info/10 border-info/20 text-info"
};
const iconElements = {
error: /* @__PURE__ */ jsx("svg", { className: "w-5 h-5 flex-shrink-0", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" }) }),
warning: /* @__PURE__ */ jsx("svg", { className: "w-5 h-5 flex-shrink-0", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16c-.77.833.192 2.5 1.732 2.5z" }) }),
info: /* @__PURE__ */ jsx("svg", { className: "w-5 h-5 flex-shrink-0", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" }) })
};
return /* @__PURE__ */ jsx(
"div",
{
className: `
pointer-events-auto transform transition-all duration-${animationDuration}
${dismissed ? "opacity-0 translate-x-full scale-95" : "opacity-100 translate-x-0 scale-100"}
`,
style: {
transitionDuration: `${animationDuration}ms`
},
children: /* @__PURE__ */ jsxs(
"div",
{
className: `
relative flex items-start gap-3 p-4 rounded-lg border shadow-lg backdrop-blur-sm
${typeStyles[displayError.type]}
`,
role: "alert",
"aria-live": "polite",
children: [
/* @__PURE__ */ jsx("div", { className: "mt-0.5", children: iconElements[displayError.type] }),
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
/* @__PURE__ */ jsx("div", { className: "font-medium text-sm mb-1", children: displayError.title }),
/* @__PURE__ */ jsx("div", { className: "text-sm opacity-90 mb-3", children: displayError.message }),
Object.keys(displayError.fieldErrors).length > 0 && /* @__PURE__ */ jsx("div", { className: "mb-3 space-y-1", children: Object.entries(displayError.fieldErrors).map(([field, error]) => /* @__PURE__ */ jsxs("div", { className: "text-xs opacity-80", children: [
/* @__PURE__ */ jsxs("span", { className: "font-medium", children: [
field,
":"
] }),
" ",
error
] }, field)) }),
displayError.actions && displayError.actions.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex gap-2 flex-wrap", children: displayError.actions.map((action, index) => /* @__PURE__ */ jsx(
"button",
{
onClick: () => handleAction(action),
className: `
px-3 py-1 text-xs rounded font-medium transition-colors
${action.type === "primary" ? "bg-current text-base-100 hover:opacity-80" : action.type === "danger" ? "bg-error text-error-content hover:bg-error/80" : "bg-current/10 hover:bg-current/20"}
`,
children: action.label
},
index
)) })
] }),
/* @__PURE__ */ jsx(
"button",
{
onClick: handleDismiss,
className: "flex-shrink-0 p-1 hover:bg-current/10 rounded transition-colors",
"aria-label": "Dismiss notification",
children: /* @__PURE__ */ jsx("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) })
}
)
]
}
)
}
);
}
function showErrorToast(displayError) {
console.warn("Error Toast (fallback):", displayError);
}
function useErrorToast() {
const toast = useToast();
const showHttpError = React.useCallback((displayError) => {
return toast.showToast(displayError);
}, [toast]);
const showSimpleError = React.useCallback((message, title = "Error") => {
const displayError = {
title,
message,
type: "error",
retryable: false,
fieldErrors: {}
};
return toast.showToast(displayError);
}, [toast]);
const showSuccess = React.useCallback((message, title = "Success") => {
const displayError = {
title,
message,
type: "info",
retryable: false,
fieldErrors: {}
};
return toast.showToast(displayError);
}, [toast]);
const showWarning = React.useCallback((message, title = "Warning") => {
const displayError = {
title,
message,
type: "warning",
retryable: false,
fieldErrors: {}
};
return toast.showToast(displayError);
}, [toast]);
return {
showHttpError,
showSimpleError,
showSuccess,
showWarning,
hideToast: toast.hideToast,
clearAll: toast.clearAllToasts
};
}
// src/hooks/useHttpClient.ts
import * as React2 from "react";
function useHttpClient(config) {
const abortControllers = React2.useRef(/* @__PURE__ */ new Set());
const client = React2.useMemo(() => {
const httpClient = new HttpClient(config);
const originalRequest = httpClient.request.bind(httpClient);
httpClient.request = async function(endpoint, requestConfig = {}) {
const controller = new AbortController();
abortControllers.current.add(controller);
try {
const result = await originalRequest(endpoint, {
...requestConfig,
signal: controller.signal
});
abortControllers.current.delete(controller);
return result;
} catch (error) {
abortControllers.current.delete(controller);
throw error;
}
};
return httpClient;
}, []);
React2.useEffect(() => {
return () => {
abortControllers.current.forEach((controller) => {
controller.abort();
});
abortControllers.current.clear();
};
}, []);
return client;
}
function useHttpRequest(client, endpoint, config) {
const [state, setState] = React2.useState({
data: null,
loading: false,
error: null
});
const abortControllerRef = React2.useRef(null);
const execute = React2.useCallback(async () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
const controller = new AbortController();
abortControllerRef.current = controller;
setState({ data: null, loading: true, error: null });
try {
const response = await client.request(endpoint, {
...config,
signal: controller.signal
});
if (!controller.signal.aborted) {
setState({ data: response.data, loading: false, error: null });
}
} catch (error) {
if (!controller.signal.aborted) {
setState({ data: null, loading: false, error });
}
}
}, [client, endpoint, config]);
React2.useEffect(() => {
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, []);
return { ...state, execute };
}
export {
ApiErrorHandler,
ApiRequestError,
ErrorCategory,
ErrorHandler,
ErrorSeverity,
GlobalErrorHandler,
HttpClient,
HttpClientError,
ToastProvider,
configureGlobalApiErrorHandler,
configureGlobalErrorHandling,
configureGlobalHttpClient,
createDismissAction,
createHttpClient,
createRefreshAction,
createRetryAction,
createSupportAction,
createToastError,
createUserFriendlyMessage,
formatFieldErrors,
getErrorColor,
getErrorIcon,
getGlobalApiErrorHandler,
getGlobalErrorHandler,
getGlobalHttpClient,
handleApiError,
handleFetchError,
humanizeErrorMessage,
installGlobalErrorHandling,
isApiError,
parseFetchError,
parseHttpError,
parseUnknownError,
shouldShowToUser,
showErrorToast,
truncateErrorMessage,
useErrorToast,
useHttpClient,
useHttpRequest,
useToast
};
//# sourceMappingURL=index.js.map