import * as React from "react"; import * as Cause from "effect/Cause"; import * as Data from "effect/Data"; import * as Exit from "effect/Exit"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; export type FrontendErrorContext = { readonly surface: string; readonly action: string; readonly message?: string; readonly severity?: "error" | "warning"; readonly metadata?: Record; }; export type FrontendErrorReporter = (error: unknown, context: FrontendErrorContext) => void; class FrontendHandledError extends Data.TaggedError("FrontendHandledError")<{ readonly cause: unknown; readonly context: FrontendErrorContext; }> {} const ErrorMessage = Schema.Struct({ message: Schema.String }); const decodeErrorMessage = Schema.decodeUnknownOption(ErrorMessage); const defaultFrontendErrorReporter: FrontendErrorReporter = (error, context) => { if (typeof globalThis.reportError !== "function") return; globalThis.reportError(new FrontendHandledError({ cause: error, context })); }; const FrontendErrorReporterContext = React.createContext( defaultFrontendErrorReporter, ); let currentFrontendErrorReporter = defaultFrontendErrorReporter; export const reportHandledFrontendError = (error: unknown, context: FrontendErrorContext): void => { currentFrontendErrorReporter(error, context); }; export const FrontendErrorReporterProvider = ( props: React.PropsWithChildren<{ reporter?: FrontendErrorReporter }>, ) => { const reporter = props.reporter ?? defaultFrontendErrorReporter; currentFrontendErrorReporter = reporter; return ( {props.children} ); }; export const useReportHandledError = (): FrontendErrorReporter => React.useContext(FrontendErrorReporterContext); export const messageFromUnknown = (error: unknown, fallback: string): string => Option.match(decodeErrorMessage(error), { onNone: () => (typeof error === "string" && error.length > 0 ? error : fallback), onSome: ({ message }) => message, }); export const messageFromExit = (exit: Exit.Exit, fallback: string): string => Option.match(Option.flatMap(Exit.findErrorOption(exit), decodeErrorMessage), { onNone: () => fallback, onSome: ({ message }) => message, }); export const reportExitFailure = ( report: FrontendErrorReporter, exit: Exit.Exit, context: FrontendErrorContext, ): void => { if (!Exit.isFailure(exit)) return; report(exit.cause, context); }; export const useErrorMessageFromExit = (): (( exit: Exit.Exit, fallback: string, context: Omit & { readonly message?: string }, ) => string) => { const report = useReportHandledError(); return React.useCallback( (exit, fallback, context) => { const message = messageFromExit(exit, fallback); reportExitFailure(report, exit, { ...context, message: context.message ?? message }); return message; }, [report], ); }; export const reportCauseFailure = ( report: FrontendErrorReporter, cause: Cause.Cause, context: FrontendErrorContext, ): void => { report(cause, context); };