import { Logger } from './logger'; import type { MapLike, SdkInitOptions, VisitorInfo } from './app-types'; import { DataStore } from './data-store'; import type { MpClientSdk } from '../models/mp-client-sdk'; const DEFAULT_RETRY_COUNT = 3; const DEFAULT_RETRY_DELAY_MS = 1000; const DEFAULT_TIMEOUT_MS = 10000; export class NetworkService { /** * Sleep for a specified duration */ private static sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Fetch with timeout wrapper */ private static async fetchWithTimeout( url: string, options: RequestInit, timeoutMs: number = DEFAULT_TIMEOUT_MS ): Promise { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(url, { ...options, signal: controller.signal, }); return response; } finally { clearTimeout(timeoutId); } } /** * Fetch visitor/identity info from server * Returns null if fetch fails (caller should handle retry logic) */ public static async fetchIdlInfo(url: string): Promise { try { const response = await this.fetchWithTimeout( url, { method: 'GET', headers: { 'Content-Type': 'text/plain', 'cache-control': 'no-store', ...DataStore.getCommonCookies(), }, }, DEFAULT_TIMEOUT_MS ); if (response && response.ok) { return response.json(); } else { Logger.logError( `Failed to fetch IDL info: ${response?.status} ${response?.statusText}` ); return null; } } catch (err) { Logger.logError('Error fetching IDL info:', err); return null; } } /** * Fetch visitor/identity info with retry logic * @param url The IDL URL * @param retries Number of retries (default: 3) * @returns VisitorInfo or null if all retries fail */ public static async fetchIdlInfoWithRetry( url: string, retries: number = DEFAULT_RETRY_COUNT ): Promise { let lastError: Error | null = null; for (let attempt = 1; attempt <= retries; attempt++) { try { Logger.logDbg(`Fetching IDL info (attempt ${attempt}/${retries})`); const result = await this.fetchIdlInfo(url); if (result) { Logger.logDbg('IDL info fetched successfully'); return result; } } catch (err) { lastError = err instanceof Error ? err : new Error(String(err)); Logger.logError(`IDL fetch attempt ${attempt} failed:`, err); } // Wait before retry (exponential backoff) if (attempt < retries) { const delay = DEFAULT_RETRY_DELAY_MS * Math.pow(2, attempt - 1); Logger.logDbg(`Retrying IDL fetch in ${delay}ms...`); await this.sleep(delay); } } Logger.logError(`All ${retries} IDL fetch attempts failed`, lastError); return null; } /** * Refresh client SDK JSON configuration with retry logic * @param sdkInitOptions SDK initialization options * @returns true if successful, throws Error if all retries fail */ public static async refreshClientSdkJson( sdkInitOptions: SdkInitOptions ): Promise { const url = `${sdkInitOptions.baseUrl}/${sdkInitOptions.projectId}${ sdkInitOptions.env === 'staging' ? '-staging' : '' }.json`; let lastError: Error | null = null; for (let attempt = 1; attempt <= DEFAULT_RETRY_COUNT; attempt++) { try { Logger.logDbg( `Fetching SDK config (attempt ${attempt}/${DEFAULT_RETRY_COUNT}): ${url}` ); const response = await this.fetchWithTimeout( url, { method: 'GET', headers: { 'Content-Type': 'text/plain', 'cache-control': 'no-store', }, }, DEFAULT_TIMEOUT_MS ); if (response && response.ok) { const sdkJson: MpClientSdk = await response.json(); Logger.logDbg('Client SDK config fetched successfully'); await DataStore.init(sdkJson); return; // Success - exit the retry loop } else { throw new Error( `HTTP ${response?.status}: ${ response?.statusText || 'Unknown error' }` ); } } catch (err) { lastError = err instanceof Error ? err : new Error(String(err)); Logger.logError(`SDK config fetch attempt ${attempt} failed:`, err); // Wait before retry (exponential backoff) if (attempt < DEFAULT_RETRY_COUNT) { const delay = DEFAULT_RETRY_DELAY_MS * Math.pow(2, attempt - 1); Logger.logDbg(`Retrying SDK config fetch in ${delay}ms...`); await this.sleep(delay); } } } // All retries failed - throw error so caller can handle it throw new Error( `Failed to fetch SDK configuration after ${DEFAULT_RETRY_COUNT} attempts: ${lastError?.message}` ); } public static sendPostRequest(url: string, body: MapLike): void { this.sendNetworkRequest('post', url, body).catch((err) => { Logger.logError(err); }); } public static sendGetRequest(url: string): void { this.sendNetworkRequest('get', url).catch((err) => { Logger.logError(err); }); } private static async sendNetworkRequest( method: 'get' | 'post', url: string, body?: MapLike ): Promise { try { Logger.logDbg( `Sending ${method} request to ${url} with body ${JSON.stringify(body)}` ); Logger.logDbg( `Common Cookies: ${JSON.stringify(DataStore.getCommonCookies() ?? {})}` ); if (method === 'get') { return await fetch(url, { method: 'GET', headers: { 'Content-Type': 'text/plain', 'cache-control': 'no-store', // 'x-dbg': 'trace', ...DataStore.getCommonCookies(), }, }); } else if (method === 'post') { return await fetch(url, { method: 'POST', headers: { 'Content-Type': 'text/plain', 'cache-control': 'no-store', // 'x-dbg': 'trace', ...DataStore.getCommonCookies(), }, body: JSON.stringify(body ?? '{}'), }); } else { throw new Error('Unsupported method: ' + method); } } catch (err) { Logger.logError(err); return null; } } }