import { type LDService } from '@servicetitan/launchdarkly-service'; import { Log } from '@servicetitan/log-service'; import { symbolToken } from '@servicetitan/react-ioc'; import { ExposedDependencies } from '@servicetitan/startup-utils'; import { EventBus } from './event-bus'; import { HistoryManager } from './history-manager'; export interface ExposedInstanceDependencies { launchDarkly?: { version: string; ldService?: LDService }; } export const BASENAME_TOKEN = symbolToken('BASENAME_TOKEN'); export const EXPOSED_DEPENDENCIES_TOKEN = symbolToken( 'EXPOSED_DEPENDENCIES_TOKEN' ); export const EXPOSED_INSTANCE_DEPENDENCIES_TOKEN = symbolToken( 'EXPOSED_INSTANCE_DEPENDENCIES_TOKEN' ); export const IS_WEB_COMPONENT_TOKEN = symbolToken('IS_WEB_COMPONENT_TOKEN'); export const RENDER_ROOT_TOKEN = symbolToken('RENDER_ROOT_TOKEN'); export interface Entries { ldService?: LDService; logService?: Log; historyManager?: HistoryManager; exposedDependencies?: ExposedDependencies; exposedInstanceDependencies?: ExposedInstanceDependencies; eventBus?: EventBus; basename?: string; onReady?: () => void; onDispose?: () => void; } export interface IWebComponent { provide(entries: Entries): void; } export interface GetJsonError { status: number; body: string; json: object; } interface GetJsonOptions extends RequestInit { retries?: number; retryDelay?: number; onRequestError?(error: GetJsonError): void; } export function createNetworkError(message: string) { const error = new Error(message); error.name = 'NetworkError'; return error; } export async function getJson( url: string, { retries = 5, retryDelay = 1000, onRequestError, ...config }: GetJsonOptions = {} ): Promise<{ data: T; headers: Headers; url: string }> { let response: Response; try { response = await fetch(url, config); } catch { if (retries > 0 && !config.signal?.aborted) { return new Promise((resolve, reject) => { const { signal } = config; const timeoutId = window.setTimeout(() => { getJson(url, { ...config, onRequestError, retries: retries - 1, retryDelay: retryDelay * 4, }).then(resolve, reject); }, retryDelay); const abortHandler = () => { window.clearTimeout(timeoutId); signal?.removeEventListener('abort', abortHandler); reject(signal?.reason); }; signal?.addEventListener('abort', abortHandler); }); } throw createNetworkError(`Failed to fetch: "${url}".`); } const responseJSON = await response.json().catch(() => ({})); if (!response.ok) { let message = `Requesting "${url}" failed with status code ${response.status}.`; try { message += ` Details: ${await response.text()}.`; } catch {} // eslint-disable-line no-empty onRequestError?.({ status: response.status, json: responseJSON, body: await response.text().catch(() => ''), }); throw createNetworkError(message); } return { data: responseJSON, headers: response.headers, url: response.url ?? url }; }