import type { APIContext, AstroGlobal } from 'astro' import type { ErrorInferenceObject } from 'astro/actions/runtime/utils.js' import type { ActionError, SafeResult } from 'astro:actions' type MessageObject = | { uiMessage: string type: 'success' origin: 'action' | 'runtime' | 'url' | `custom_${string}` error?: undefined } | ({ uiMessage: string type: 'error' } & ( | { origin: 'action' // eslint-disable-next-line @typescript-eslint/no-explicit-any error: ActionError } | { origin: 'runtime'; error?: unknown } | { origin: 'url'; error?: undefined } | { origin: `custom_${string}`; error?: unknown } )) /** * Helper class for handling errors and success messages. * It automatically adds messages from the query string to the messages array. * * @example * ```astro * --- * const messages = new ErrorBanners(Astro) * const data = await messages.try('Oops!', () => fetchData()) * --- * * {data ? data : 'No data'} * * ``` */ export class ErrorBanners { private _messages: MessageObject[] = [] constructor(messages?: MessageObject[]) { this._messages = messages ?? [] } /** * Get all stored messages. */ public get get() { return [...this._messages] } /** * Get all error messages. */ public get errors() { return this._messages.filter((msg) => msg.type === 'error').map((error) => error.uiMessage) } /** * Get all success messages. */ public get successes() { return this._messages.filter((msg) => msg.type === 'success').map((success) => success.uiMessage) } /** * Handler for errors. Intended to be used in `.catch()` blocks. * * @example * ```ts * const data = await fetchData().catch(messages.handler('Oops!')) * ``` * * @param uiMessage The UI message to display, or function to generate it. * @returns The handler function. */ public handler(uiMessage: string | ((error: unknown) => string)) { return (error: unknown) => { const message = typeof uiMessage === 'function' ? uiMessage(error) : uiMessage console.error(`[ErrorBanners] ${message}`, error) this._messages.push({ uiMessage: message, type: 'error', error, origin: 'runtime', }) } } /** * Run a function and catch its errors. * * @example * ```ts * const items = await messages.try("Oops!", () => fetchItems()) // Item[] | undefined * const items = await messages.try("Oops!", () => fetchItems(), []) // Item[] * ``` * * @param uiMessage The UI message to display, or function to generate it. * @param fn The function to execute. * @param fallback The fallback value. * @returns The result of the function. */ public async try( uiMessage: string | ((error: unknown) => string), fn: () => Promise | T, fallback?: F ) { try { const result = await fn() return result } catch (error) { this.handler(uiMessage)(error) return fallback as F extends never[] ? T extends [infer _First, ...infer _Rest] ? [] : T extends unknown[] ? T[number][] : F : F } } /** * Run multiple functions in parallel and catch errors. * * @example * ```ts * const [categories, posts] = await messages.tryMany([ * ["Error in categories", () => fetchCategories(), []], * ["Error in posts", () => fetchPosts()] * ]) * ``` * * @param operations The operations to run. * @returns The results of the operations. */ public async tryMany>[] | []>( operations: T ) { const results = await Promise.all(operations.map((args) => this.try(...args))) return results as unknown as Promise<{ -readonly [P in keyof T]: Awaited> | T[P][2] }> } /** * Add one or multiple messages. */ public add(...messages: (MessageObject | null | undefined)[]) { messages .filter((message) => message !== null && message !== undefined) .forEach((message) => { this._messages.push(message) if (message.type === 'error') { console.error(`[ErrorBanners] ${message.uiMessage}`, message.error) } }) } /** * Add a success message. */ public addSuccess(message: string) { this._messages.push({ uiMessage: message, type: 'success', origin: 'runtime', }) } /** * Add a success message if the action result is successful. */ public addIfSuccess( actionResult: SafeResult | undefined, message: string | ((actionResult: TOutput) => string) ) { if (actionResult && !actionResult.error) { const actualMessage = typeof message === 'function' ? message(actionResult.data) : message this._messages.push({ uiMessage: actualMessage, type: 'success', origin: 'action', }) } } /** * Clear all messages. */ public clear() { this._messages = [] } } /** * Generate message objects for {@link ErrorBanners} from the URL. */ export function getMessagesFromUrl( astro: Pick | Readonly, 'url'> ) { const messages: MessageObject[] = [] // Get error messages messages.push( ...astro.url.searchParams.getAll('error').map( (error) => ({ uiMessage: error, type: 'error', origin: 'url', }) as const satisfies MessageObject ) ) // Get success messages messages.push( ...astro.url.searchParams.getAll('success').map( (success) => ({ uiMessage: success, type: 'success', origin: 'url', }) as const satisfies MessageObject ) ) return messages }