/** Raised on the window when a Recomaze service stops or starts answering. */ export const CONNECTION_EVENT = 'recomaze:connection'; /** What a call that never got an answer says, instead of axios's "Network Error". */ export const UNREACHABLE_MESSAGE = 'Recomaze could not be reached. Check your connection, then try again.'; /** Gateway answers: the service behind them is down or restarting. */ const DOWN_STATUSES: ReadonlySet = new Set([502, 503, 504]); /** The part of an axios error this module reads. */ export interface FailedCall { code?: string; message?: string; response?: { status?: number; data?: unknown }; config?: { url?: string; baseURL?: string }; } /** Detail of {@link CONNECTION_EVENT}. */ export interface ConnectionChange { host: string; up: boolean; } /** * The Recomaze host a request went to, or '' for anything else: WordPress's * own REST API failing is not Recomaze being down. * * @param {string | undefined} url - The request URL, absolute or relative. * @param {string | undefined} baseURL - The base a relative URL resolves against. * @returns {string} The host, e.g. ``agent.recomaze.ai``. */ export const recomazeHost = (url?: string, baseURL?: string): string => { try { const host = new URL(url ?? '', baseURL || undefined).hostname; return /(^|\.)recomaze\.ai$/.test(host) ? host : ''; } catch { return ''; } }; /** * Whether a failed call means the service did not answer at all: no response * (refused, offline, timed out) or a gateway error. A 4xx or a 500 is the * service answering, so the page's own message stands. * * @param {FailedCall} error - The failed call. * @returns {boolean} True when the service itself is unreachable. */ export const isUnreachable = (error: FailedCall): boolean => { if (error.code === 'ERR_CANCELED') return false; const status = error.response?.status; return status === undefined || DOWN_STATUSES.has(status); }; /** * A failed call's message as a page can print it: on a refusal (4xx) the * service's own words, which are written for the merchant; on a server * error, what happened in plain words, since its text ("Internal Server * Error") is not. Never axios's "Request failed with status code 500". * * @param {FailedCall} error - The failed call. * @returns {string} The message. */ export const readableFailure = (error: FailedCall): string => { const status = error.response?.status; if (status === undefined) return UNREACHABLE_MESSAGE; const data = (error.response?.data ?? {}) as { detail?: unknown; message?: unknown; }; if (status < 500) { if (typeof data.detail === 'string' && data.detail) return data.detail; if (typeof data.message === 'string' && data.message) return data.message; } return `Recomaze could not complete that (error ${status}). Try again in a moment.`; }; /** * Tell the page a Recomaze service answered or did not. * * @param {ConnectionChange} change - The host and whether it answered. * @returns {void} */ export const reportConnection = (change: ConnectionChange): void => { if (!change.host || typeof window === 'undefined') return; window.dispatchEvent( new CustomEvent(CONNECTION_EVENT, { detail: change }) ); };