UNPKG

every-plugin

Version:
333 lines (280 loc) 9.78 kB
import { ORPCError } from "@orpc/contract"; import { Cause, Data } from "effect"; import type { z } from "zod"; export class PluginRuntimeError extends Data.TaggedError("PluginRuntimeError")<{ readonly pluginId?: string; readonly operation?: string; readonly procedureName?: string; readonly cause?: Error; readonly retryable: boolean; }> {} export class ModuleFederationError extends Data.TaggedError("ModuleFederationError")<{ readonly pluginId: string; readonly remoteUrl: string; readonly cause?: Error; }> {} export class ValidationError extends Data.TaggedError("ValidationError")<{ readonly pluginId: string; readonly stage: "config" | "input" | "output" | "state"; readonly zodError: z.ZodError; }> {} const extractErrorMessage = (error: unknown): string => { if (!error) return "Unknown error"; if (error instanceof Error) { if (error.message) return error.message; if ((error as any).cause instanceof Error) { return extractErrorMessage((error as any).cause); } } if (error instanceof AggregateError && error.errors?.length) { return error.errors.map((e) => extractErrorMessage(e)).join("; "); } if (typeof error === "object" && "message" in error) { return String((error as any).message); } return String(error); }; const formatValidationIssue = (issue: any, index: number, maxDisplay: number): string => { if (index >= maxDisplay) return ""; const path = Array.isArray(issue.path) && issue.path.length > 0 ? issue.path.join(".") : issue.path || "root"; const message = issue.message || "Validation failed"; return `│ ${index + 1}. ${path}: ${message}`; }; const formatDataPreview = (data: unknown, maxLength = 100): string => { if (!data) return "undefined"; try { const str = JSON.stringify(data); if (str.length <= maxLength) return str; if (typeof data === "object" && data !== null) { if (Array.isArray(data)) { return `Array(${data.length}) [...]`; } const keys = Object.keys(data); return `{ ${keys.slice(0, 3).join(", ")}${keys.length > 3 ? ", ..." : ""} }`; } return `${str.slice(0, maxLength)}...`; } catch { return String(data).slice(0, maxLength); } }; const formatORPCValidationError = (error: any): string[] | null => { const cause = error?.cause || error; if (!cause?.issues || !Array.isArray(cause.issues) || cause.issues.length === 0) { return null; } const lines: string[] = []; const errorType = error?.message || cause?.message || "Validation failed"; lines.push(`\n╭─ oRPC Validation Error ${"─".repeat(30)}`); lines.push(`│ ${errorType}`); lines.push(`│`); const maxDisplay = 10; const totalIssues = cause.issues.length; lines.push(`│ Issues (${totalIssues}):`); const displayedIssues = cause.issues.slice(0, maxDisplay); displayedIssues.forEach((issue: any, idx: number) => { const formatted = formatValidationIssue(issue, idx, maxDisplay); if (formatted) lines.push(formatted); }); if (totalIssues > maxDisplay) { lines.push(`│ ... and ${totalIssues - maxDisplay} more`); } if (cause.data !== undefined) { lines.push(`│`); lines.push(`│ Data preview: ${formatDataPreview(cause.data, 80)}`); } lines.push(`╰${"─".repeat(50)}\n`); return lines; }; export const formatORPCError = (error: any): void => { if (!(error instanceof ORPCError)) { return; } const validationLines = formatORPCValidationError(error); if (validationLines) { console.error(validationLines.join("\n")); return; } const lines: string[] = []; const code = error.code || "UNKNOWN"; const status = error.status || 500; const message = error.message || "An error occurred"; lines.push(`\n╭─ oRPC Error ${"─".repeat(40)}`); lines.push(`│ ${message}`); lines.push(`│ Code: ${code} (${status})`); lines.push(`│`); if (error.data) { const dataType = typeof error.data; if (dataType === "object" && error.data !== null) { if ("retryAfter" in error.data) { lines.push(`│ Retry after: ${error.data.retryAfter} seconds`); } if ("remainingRequests" in error.data) { lines.push(`│ Remaining: ${error.data.remainingRequests} requests`); } if ("host" in error.data) { lines.push(`│ Host: ${error.data.host}`); } if ("port" in error.data) { lines.push(`│ Port: ${error.data.port}`); } if ("suggestion" in error.data) { lines.push(`│ → ${error.data.suggestion}`); } if ("resource" in error.data) { lines.push(`│ Resource: ${error.data.resource}`); } if ("resourceId" in error.data) { lines.push(`│ ID: ${error.data.resourceId}`); } } } switch (code) { case "UNAUTHORIZED": lines.push(`│ → Check your API key or credentials`); break; case "TOO_MANY_REQUESTS": lines.push(`│ → Wait before retrying`); break; case "SERVICE_UNAVAILABLE": case "BAD_GATEWAY": case "GATEWAY_TIMEOUT": lines.push(`│ → The service may be temporarily unavailable`); break; case "TIMEOUT": lines.push(`│ → The operation took too long`); break; } lines.push(`╰${"─".repeat(50)}\n`); console.error(lines.join("\n")); }; const formatPluginError = ( pluginId: string | undefined, operation: string | undefined, message: string, ): void => { const lines: string[] = []; lines.push(`\n╭─ Plugin Error ${"─".repeat(40)}`); if (pluginId) lines.push(`│ Plugin: ${pluginId}`); if (operation) lines.push(`│ During: ${operation}`); lines.push(`│`); if (message.includes("ECONNREFUSED")) { lines.push(`│ ❌ Connection refused`); lines.push(`│ `); lines.push(`│ A required service is not running.`); lines.push(`│ → Run: docker compose up -d`); } else if (message.includes("ENOTFOUND")) { lines.push(`│ ❌ Host not found`); lines.push(`│ `); lines.push(`│ Check your connection URL or network settings.`); } else if (message.includes("ETIMEDOUT") || message.includes("timeout")) { lines.push(`│ ❌ Connection timeout`); lines.push(`│ `); lines.push(`│ The service took too long to respond.`); } else if (message.includes("EACCES") || message.includes("permission")) { lines.push(`│ ❌ Permission denied`); lines.push(`│ `); lines.push(`│ Check credentials or access permissions.`); } else if (message.includes("401") || message.includes("unauthorized")) { lines.push(`│ ❌ Authentication failed`); lines.push(`│ `); lines.push(`│ Check your API key or credentials.`); } else { lines.push(`│ ❌ ${message}`); } lines.push(`╰${"─".repeat(50)}\n`); console.error(lines.join("\n")); }; const isRetryableError = (message: string): boolean => { const retryablePatterns = ["ETIMEDOUT", "ECONNRESET", "timeout", "503", "429"]; return retryablePatterns.some((p) => message.toLowerCase().includes(p.toLowerCase())); }; // Helper to determine if an oRPC error code is retryable export const isRetryableORPCCode = (code: string): boolean => { switch (code) { case "TOO_MANY_REQUESTS": case "SERVICE_UNAVAILABLE": case "BAD_GATEWAY": case "GATEWAY_TIMEOUT": case "TIMEOUT": return true; default: return false; } }; // Convert ORPC errors from plugin procedures to PluginRuntimeError export const wrapORPCError = ( orpcError: ORPCError<string, unknown>, pluginId?: string, procedureName?: string, operation?: string, ): PluginRuntimeError => { const validationLines = formatORPCValidationError(orpcError); if (validationLines) { console.error(validationLines.join("\n")); } return new PluginRuntimeError({ pluginId, operation, procedureName, retryable: isRetryableORPCCode(orpcError.code), cause: orpcError as Error, }); }; /** * Extracts the underlying error from Effect's FiberFailure wrapper. * When Effect.runPromise rejects, errors are wrapped in FiberFailure. * This extracts the original error so oRPC can handle it properly. */ export const extractFromFiberFailure = (error: unknown): unknown => { if (!error || typeof error !== "object") return error; if ("cause" in error) { const cause = (error as { cause?: unknown }).cause; if (cause && typeof cause === "object" && "_tag" in cause) { try { const squashed = Cause.squash(cause as Cause.Cause<unknown>); if (squashed instanceof ORPCError) { return squashed; } return squashed; } catch { // Not a valid Cause } } if (cause instanceof ORPCError) { return cause; } } return error; }; // Universal error converter for the runtime export const toPluginRuntimeError = ( error: unknown, pluginId?: string, procedureName?: string, operation?: string, defaultRetryable = false, ): PluginRuntimeError => { if (error instanceof ORPCError) { return wrapORPCError(error, pluginId, procedureName, operation); } if (error instanceof PluginRuntimeError) { return error; } const validationLines = formatORPCValidationError(error); if (validationLines) { console.error(validationLines.join("\n")); } else { const message = extractErrorMessage(error); formatPluginError(pluginId, operation, message); } return new PluginRuntimeError({ pluginId, operation, procedureName, retryable: defaultRetryable || isRetryableError(extractErrorMessage(error)), cause: error instanceof Error ? error : new Error(extractErrorMessage(error)), }); };