UNPKG

@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
// 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 = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;", "/": "&#x2F;" }; 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