import { parseRegexSingle } from "@foxxmd/regex-buddy-core"; import mergeErrorCause from 'merge-error-cause'; import { findCauseByFunc, isAbortReasonErrorLike } from "../../utils/ErrorUtils.ts"; import { UpstreamError, type UpstreamErrorOptions } from "./UpstreamError.ts"; import {addKnownErrorConstructor, serializeError} from 'serialize-error'; import type {LifecycleInput} from "../../../core/Atomic.ts"; export abstract class NamedError extends Error { public abstract name: string; public toJSON() { return serializeError(this); } } export abstract class StageError extends NamedError {} export class BuildDataError extends StageError { name = 'Init Build Data'; } addKnownErrorConstructor(BuildDataError); export class ParseCacheError extends StageError { name = 'Init Parse Cache'; } addKnownErrorConstructor(ParseCacheError); export class TransformRulesError extends StageError { name = 'Transform Rules'; } addKnownErrorConstructor(TransformRulesError); export class ConnectionCheckError extends StageError { name = 'Connection Check'; } addKnownErrorConstructor(ConnectionCheckError); export class AuthError extends NamedError { override name = 'Authentication Error'; unrecoverable: boolean = false; public constructor(msg?: string, opts: ErrorOptions & {unrecoverable?: boolean} = {}) { super(msg, opts); this.unrecoverable = opts.unrecoverable ?? false; } } addKnownErrorConstructor(AuthError); export const isAuthError = (e: Error): e is AuthError => e.name === 'Authentication Error' || e instanceof AuthError; export class AuthCheckError extends StageError { override name = 'Authentication Check'; unrecoverable?: boolean; public constructor(msg?: string, opts: ErrorOptions & {unrecoverable?: boolean} = {}) { super(msg, opts); this.unrecoverable = opts.unrecoverable; } } addKnownErrorConstructor(AuthCheckError); export const isAuthCheckError = (e: Error): e is AuthCheckError => e.name === 'Authentication Check' || e instanceof AuthCheckError; export class PostInitError extends StageError { name = 'Post Initialization'; } addKnownErrorConstructor(PostInitError); export class StageChangeError extends NamedError { override name = 'Stage Change'; } const STACK_AT_REGEX = new RegExp(/[\n\r]\s*at/); export class SimpleError extends Error implements HasSimpleError { simple: boolean; override name = 'SimpleError'; public toJSON() { return serializeError(this); } stackShortened: boolean = false; shortenStack() { if(this.stack !== undefined) { const atIndex = parseRegexSingle(STACK_AT_REGEX, this.stack); if(atIndex !== undefined) { const firstn = this.stack.indexOf('\n', atIndex.index + atIndex.match.length); if(firstn !== -1) { this.stack = this.stack.slice(0, firstn); this.stackShortened = true; } } } } public constructor(msg: string, options?: ErrorOptions & { simple?: boolean, shortStack?: boolean }) { super(msg, options); const { simple = true, shortStack = false } = options || {}; this.simple = simple; Error.captureStackTrace(this, this.constructor); if(shortStack) { this.shortenStack(); } } } addKnownErrorConstructor(SimpleError, () => new SimpleError('')) export class StageTransformError extends NamedError { override name = 'Stage Transform'; lifecycleInputs?: LifecycleInput[] stageName: string; constructor(name: string, message: string, options: ErrorOptions & {inputs?: LifecycleInput[]} = {}) { const { inputs, ...restOpts } = options; super(message, restOpts); this.lifecycleInputs = inputs; this.stageName = name; } } addKnownErrorConstructor(StageTransformError, () => new StageTransformError('','')) export class SkipTransformStageError extends SimpleError { lifecycleInputs?: LifecycleInput[] override name = 'Skip Transform Stage'; public constructor(msg: string, options: ErrorOptions & { simple?: boolean, shortStack?: boolean, inputs?: LifecycleInput[]} = {}) { const { inputs, ...restOpts } = options; super(msg, restOpts); this.lifecycleInputs = inputs; } } addKnownErrorConstructor(SkipTransformStageError, () => new SkipTransformStageError('')) export class StagePrerequisiteError extends SimpleError { lifecycleInputs?: LifecycleInput[] override name = 'Stage Prerequisite'; public constructor(msg: string, options: ErrorOptions & { simple?: boolean, shortStack?: boolean, inputs?: LifecycleInput[]} = {}) { const { inputs, ...restOpts } = options; super(msg, restOpts); this.lifecycleInputs = inputs; } } addKnownErrorConstructor(StagePrerequisiteError, () => new StagePrerequisiteError('')) export interface HasSimpleError extends Error { simple: boolean } export const isSimpleError = (e: unknown): e is HasSimpleError => { if(!(e instanceof Error)) { return false; } return 'simple' in e; } export const mergeSimpleError = (err: Error): Error => { const anySimple = findCauseByFunc(err, (e) => isSimpleError(e)); if(anySimple && anySimple.simple) { // mergeErrorCause mutates the argument // and we want to be able to do more cause/error parsing after merging for logging // so give it a copy instead of the original const mergedErr = mergeErrorCause(structuredClone(err)); if(!('toJson' in mergedErr)) { // @ts-expect-error this is used by serialize error with out custom errors mergedErr.toJSON = () => serializeError(mergedErr); } return mergedErr; } return err; } export class ScrobbleSubmitError extends UpstreamError { override name = 'Scrobble Submit Error'; payload?: T; constructor(message: string, options?: UpstreamErrorOptions & {payload?: T}) { super(message, options); this.payload = options?.payload; } } addKnownErrorConstructor(ScrobbleSubmitError, () => new ScrobbleSubmitError('')) export class AbortedError extends SimpleError { override name = 'Aborted Operation'; } addKnownErrorConstructor(AbortedError, () => new AbortedError('')); export const generateLoggableAbortReason = (msg: string, signal: AbortSignal): AbortedError => { const reason = signal.reason; let err: AbortedError; if(isAbortReasonErrorLike(signal)) { err = new AbortedError(msg, {cause: reason}); } else { err = new AbortedError(`${msg} => ${reason ?? 'No Reason Given'}`, {simple: true, shortStack: true}); } Error.captureStackTrace(err, generateLoggableAbortReason); return err; } export class InvalidRegexError extends SimpleError { override name = 'Invalid Regex Error'; constructor(regex: RegExp | RegExp[], val?: string, url?: string, message?: string) { const msgParts = [ message ?? 'Regex(es) did not match the value given.', ]; const regArr = Array.isArray(regex) ? regex : [regex]; for(const r of regArr) { msgParts.push(`Regex: ${r}`) } if (val !== undefined) { msgParts.push(`Value: ${val}`); } if (url !== undefined) { msgParts.push(`Sample regex: ${url}`); } super(msgParts.join('\r\n')); } } addKnownErrorConstructor(InvalidRegexError, () => new InvalidRegexError(new RegExp(/1/))); export type AuthErrorMap = { root: AuthError check: AuthCheckError auth: AuthError | AuthCheckError parent: Error } export const findAuthIssue = (err: Error, opts: { type?: T, unrecoverable?: boolean } = {}): AuthErrorMap[T] | undefined => { const { type = 'auth', unrecoverable } = opts; const authError = findCauseByFunc(err, (e) => { if (['check','auth'].includes(type) && isAuthCheckError(e)) { return unrecoverable === undefined || e.unrecoverable === unrecoverable; } if (['root','auth'].includes(type) && isAuthError(e)) { return unrecoverable === undefined || e.unrecoverable === unrecoverable; } return false; }); if (authError !== undefined) { if (type === 'parent') { return err as AuthErrorMap[T]; } return authError as AuthErrorMap[T]; } return undefined; }